6 Commits
Author SHA1 Message Date
Konrad Urdahl Halnum 0a2ce30edd refactor(Card): change card field from disable to active for intuitive admin 2022-10-12 23:54:04 +02:00
Konrad Urdahl Halnum 4484195ddb feat: add card model 2022-10-12 23:45:09 +02:00
nikolark 79fe5cac0f 🔒 Audit 2022-04-17 21:29:02 +02:00
nikolark 95d7d13f38 ✨ Handle user not found 2022-04-17 21:13:22 +02:00
nikolark ce2cdfd409 ⚡ Code split routes 2022-04-17 16:59:00 +02:00
nikolark b56d103ae7 Merge branch 'feature/synthwave' into 'main'
✨ BBS 2222 synthwave theme

See merge request edb/bbs!3
2022-04-17 14:39:44 +00:00
19 changed files with 124 additions and 43 deletions

No files matched your search

+3 -2
View File
@@ -11,6 +11,7 @@ CACHE/
media/ media/
tmp/ tmp/
frontend/dist frontend/dist
static/modern static/assets
static/index.html
.python-version .python-version
.env .env
+7
View File
@@ -7,9 +7,16 @@ class ProductAdmin(admin.ModelAdmin):
pass pass
class CardAdmin(admin.ModelAdmin):
list_display = ("name", "user", "card_id", "active")
list_filter = ("active",)
search_fields = ("user__username", "name")
admin.site.register(Product, ProductAdmin) admin.site.register(Product, ProductAdmin)
admin.site.register(InventoryTransaction) admin.site.register(InventoryTransaction)
admin.site.register(Order) admin.site.register(Order)
admin.site.register(OrderLine) admin.site.register(OrderLine)
admin.site.register(Transaction) admin.site.register(Transaction)
admin.site.register(UserProfile) admin.site.register(UserProfile)
admin.site.register(Card, CardAdmin)
+15 -1
View File
@@ -1,4 +1,6 @@
import strawberry import strawberry
from django.core.exceptions import ObjectDoesNotExist
from strawberry.extensions import Extension
from bbs.api.mutations.shop import ShopMutations from bbs.api.mutations.shop import ShopMutations
from bbs.api.mutations.users import UsersMutations from bbs.api.mutations.users import UsersMutations
@@ -17,4 +19,16 @@ class Mutation(ShopMutations, UsersMutations):
pass pass
schema = strawberry.Schema(query=Query, mutation=Mutation) class HandleNotFound(Extension):
def on_request_end(self):
result = self.execution_context.result
if result.errors:
for error in result.errors:
if isinstance(error.original_error, ObjectDoesNotExist):
error.extensions = {
"code": "NOT_FOUND",
"model": type(error.original_error).__qualname__.replace(".DoesNotExist", ""),
}
schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[HandleNotFound])
+13
View File
@@ -245,3 +245,16 @@ class UserProfile(models.Model):
def __str__(self): def __str__(self):
return f"{self.user}" return f"{self.user}"
class Card(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="cards")
card_id = models.CharField(max_length=32)
name = models.CharField(max_length=64)
# pin = models.IntegerField(max_length=4, blank=True, null=True)
# valid_until = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name}"
+6 -6
View File
@@ -8776,9 +8776,9 @@
} }
}, },
"node_modules/log-update/node_modules/ansi-regex": { "node_modules/log-update/node_modules/ansi-regex": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@@ -19172,9 +19172,9 @@
"dev": true "dev": true
}, },
"ansi-regex": { "ansi-regex": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true "dev": true
}, },
"cli-cursor": { "cli-cursor": {
-1
View File
@@ -7,7 +7,6 @@ import { Routes } from "./Routes";
import client from "./client"; import client from "./client";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { Splash as Splash } from "./components/Splash";
import { Toast } from "./components/Toast"; import { Toast } from "./components/Toast";
import { ToastProvider } from "./components/ToastProvider"; import { ToastProvider } from "./components/ToastProvider";
import { Wireframe } from "./components/Wireframe"; import { Wireframe } from "./components/Wireframe";
+24 -22
View File
@@ -1,28 +1,30 @@
import React from "react"; import React from "react";
import { Route, Routes as ReactRouterRoutes } from "react-router-dom"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom";
import { Splash } from "./components/Splash"; const Splash = React.lazy(() => import("./components/Splash"));
import { UserCreate } from "./features/auth/UserCreate"; const UserCreate = React.lazy(() => import("./features/auth/UserCreate"));
import { Deposit } from "./features/deposit/Deposit"; const Deposit = React.lazy(() => import("./features/deposit/Deposit"));
import { Log } from "./features/log/Log"; const Log = React.lazy(() => import("./features/log/Log"));
import { Register } from "./features/register/Register"; const Register = React.lazy(() => import("./features/register/Register"));
import { OrderStats } from "./features/stats/OrderStats"; const OrderStats = React.lazy(() => import("./features/stats/OrderStats"));
import { ProductStats } from "./features/stats/ProductStats"; const ProductStats = React.lazy(() => import("./features/stats/ProductStats"));
import { Stats } from "./features/stats/Stats"; const Stats = React.lazy(() => import("./features/stats/Stats"));
import { UserDetail } from "./features/users/UserDetail"; const UserDetail = React.lazy(() => import("./features/users/UserDetail"));
import { UserList } from "./features/users/UserList"; const UserList = React.lazy(() => import("./features/users/UserList"));
export const Routes = (): JSX.Element => ( export const Routes = (): JSX.Element => (
<ReactRouterRoutes> <React.Suspense fallback={<div>Loading...</div>}>
<Route path="/deposit" element={<Deposit />} /> <ReactRouterRoutes>
<Route path="/log" element={<Log />} /> <Route path="/deposit" element={<Deposit />} />
<Route path="/stats" element={<Stats />} /> <Route path="/log" element={<Log />} />
<Route path="/stats/orders" element={<OrderStats />} /> <Route path="/stats" element={<Stats />} />
<Route path="/stats/products" element={<ProductStats />} /> <Route path="/stats/orders" element={<OrderStats />} />
<Route path="/users" element={<UserList />} /> <Route path="/stats/products" element={<ProductStats />} />
<Route path="/users/create" element={<UserCreate />} /> <Route path="/users" element={<UserList />} />
<Route path="/user/:userId" element={<UserDetail />} /> <Route path="/users/create" element={<UserCreate />} />
<Route path="/" element={<Register />} /> <Route path="/user/:userId" element={<UserDetail />} />
<Route path="*" element={<Splash brandText="404" noOverlay />} /> <Route path="/" element={<Register />} />
</ReactRouterRoutes> <Route path="*" element={<Splash brandText="404" noOverlay />} />
</ReactRouterRoutes>
</React.Suspense>
); );
@@ -0,0 +1,9 @@
.errorMessage {
font-family: Rocket Rinder, monospace;
font-size: 3rem;
font-style: italic;
color: var(--brand);
text-align: center;
width: 100%;
margin-top: 30vh;
}
+23
View File
@@ -0,0 +1,23 @@
import { ApolloError } from "@apollo/client";
import React from "react";
import styles from "./GQLError.module.css";
const formatModel = (text: string) => {
const withSpaces = text
.replace(/([A-Z])/g, " $1")
.toLocaleLowerCase()
.trim();
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
};
export const GQLError = ({ error }: { error: ApolloError }) => {
if (error?.graphQLErrors?.length == 1) {
const single = error?.graphQLErrors[0];
if (single?.extensions?.code === "NOT_FOUND") {
const modelPretty = formatModel(single?.extensions?.model as string);
return <div className={styles.errorMessage}>{`${modelPretty} not found`}</div>;
}
}
return <div className={styles.errorMessage}>Error...</div>;
};
+2 -1
View File
@@ -8,7 +8,7 @@ interface SplashProps {
noOverlay?: boolean; noOverlay?: boolean;
} }
export const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => ( const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => (
<div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}> <div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}>
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.sun}></div> <div className={styles.sun}></div>
@@ -17,3 +17,4 @@ export const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): J
</div> </div>
</div> </div>
); );
export default Splash;
+2 -1
View File
@@ -11,7 +11,7 @@ type FormValues = {
email: string; email: string;
}; };
export const UserCreate = () => { const UserCreate = () => {
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -48,3 +48,4 @@ export const UserCreate = () => {
</div> </div>
); );
}; };
export default UserCreate;
+2 -1
View File
@@ -4,7 +4,7 @@ import { UserCard } from "../users/UserCard";
import styles from "./Deposit.module.css"; import styles from "./Deposit.module.css";
import { useDeposit } from "./useDeposit"; import { useDeposit } from "./useDeposit";
export const Deposit = (): JSX.Element => { const Deposit = (): JSX.Element => {
const { const {
loading, loading,
selectedUser, selectedUser,
@@ -65,3 +65,4 @@ export const Deposit = (): JSX.Element => {
</div> </div>
); );
}; };
export default Deposit;
+2 -1
View File
@@ -9,7 +9,7 @@ import styles from "./Log.module.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export const Log = () => { const Log = () => {
const types = { const types = {
orderOffset: Number, orderOffset: Number,
transactionOffset: Number, transactionOffset: Number,
@@ -139,3 +139,4 @@ export const Log = () => {
</div> </div>
); );
}; };
export default Log;
+2 -1
View File
@@ -8,7 +8,7 @@ import { useRegister } from "./useRegister";
const USER_EXTERNAL = "external"; const USER_EXTERNAL = "external";
export const Register = (): JSX.Element => { const Register = (): JSX.Element => {
const { const {
users, users,
products, products,
@@ -80,3 +80,4 @@ export const Register = (): JSX.Element => {
</div> </div>
); );
}; };
export default Register;
+2 -1
View File
@@ -35,7 +35,7 @@ const OrderBar = ({ data }: { data: BarDatum[] }) => (
/> />
); );
export const OrderStats = () => { const OrderStats = () => {
const { data, loading } = useQuery(OrderStatsDocument); const { data, loading } = useQuery(OrderStatsDocument);
return ( return (
@@ -77,3 +77,4 @@ export const OrderStats = () => {
</StatsView> </StatsView>
); );
}; };
export default OrderStats;
+2 -1
View File
@@ -11,7 +11,7 @@ type UserCount = {
count: number; count: number;
}; };
export const ProductStats = () => { const ProductStats = () => {
const { data, loading } = useQuery(AllProductStatsDocument, { const { data, loading } = useQuery(AllProductStatsDocument, {
variables: { active: true }, variables: { active: true },
}); });
@@ -38,3 +38,4 @@ export const ProductStats = () => {
</StatsView> </StatsView>
); );
}; };
export default ProductStats;
+2 -1
View File
@@ -5,7 +5,7 @@ import React from "react";
import { ProductStatsDocument } from "../../types"; import { ProductStatsDocument } from "../../types";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
export const Stats = () => { const Stats = () => {
const { data, loading } = useQuery(ProductStatsDocument); const { data, loading } = useQuery(ProductStatsDocument);
const series: any[] = []; const series: any[] = [];
@@ -68,3 +68,4 @@ export const Stats = () => {
</StatsView> </StatsView>
); );
}; };
export default Stats;
+6 -2
View File
@@ -3,16 +3,19 @@ import { Line } from "@nivo/line";
import React from "react"; import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { GQLError } from "../../components/GQLError";
import { User, UserDetailDocument, UserProfile } from "../../types"; import { User, UserDetailDocument, UserProfile } from "../../types";
import { formatMonth, formatTime } from "../../utils/time"; import { formatMonth, formatTime } from "../../utils/time";
import styles from "./UserDetail.module.css"; import styles from "./UserDetail.module.css";
export const UserDetail = () => { const UserDetail = () => {
const { userId } = useParams(); const { userId } = useParams();
const { data, loading } = useQuery(UserDetailDocument, { variables: { userId: userId || "" }, skip: !userId }); const { data, loading, error } = useQuery(UserDetailDocument, { variables: { userId: userId || "" }, skip: !userId });
if (loading) return <div>Loading...</div>; if (loading) return <div>Loading...</div>;
if (error) return <GQLError error={error} />;
const user = data?.user as User; const user = data?.user as User;
const profile = user?.profile as UserProfile; const profile = user?.profile as UserProfile;
// FIXME: types // FIXME: types
@@ -108,3 +111,4 @@ export const UserDetail = () => {
</div> </div>
); );
}; };
export default UserDetail;
+2 -1
View File
@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
import { AllUsersDocument } from "../../types"; import { AllUsersDocument } from "../../types";
import { activeUserFilter } from "../../utils/users"; import { activeUserFilter } from "../../utils/users";
export const UserList = () => { const UserList = () => {
const { data, loading } = useQuery(AllUsersDocument); const { data, loading } = useQuery(AllUsersDocument);
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
@@ -35,3 +35,4 @@ export const UserList = () => {
</div> </div>
); );
}; };
export default UserList;