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:
mashen
2026-04-09 12:06:14 +00:00
commit cb491e8b87
49 changed files with 1804 additions and 0 deletions

2
frontend/.env.example Normal file
View File

@@ -0,0 +1,2 @@
REACT_APP_API_URL=http://localhost:8000
REACT_APP_ENV=development

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

31
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM node:18-alpine as build
# Set work directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy project
COPY . .
# Build
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files
COPY --from=build /app/build /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

37
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /graphql {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /media {
proxy_pass http://backend:8000;
}
location /static {
proxy_pass http://backend:8000;
}
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
}

48
frontend/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "react-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.6.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-scripts": "5.0.1",
"styled-components": "^6.1.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"eslint": "^8.55.0",
"prettier": "^3.1.0"
},
"proxy": "http://localhost:8000"
}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="React + Django App" />
<title>React + Django App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

35
frontend/src/App.js Normal file
View 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
View 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>
);

View 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;

View 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';

View 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;

View 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;

View 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;

13
frontend/start.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
echo "🚀 Starting React Frontend..."
# 检查 node_modules
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# 启动开发服务器
echo "🎉 Starting development server on http://localhost:3000"
npm start