diff --git a/package.json b/package.json
index ff5a532..7f60937 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,9 @@
"@tanstack/react-router-devtools": "^1.130.13",
"@tanstack/react-router-with-query": "^1.130.12",
"@tanstack/react-start": "^1.130.15",
+ "@tiptap/pm": "^3.4.3",
+ "@tiptap/react": "^3.4.3",
+ "@tiptap/starter-kit": "^3.4.3",
"@types/ioredis": "^4.28.10",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.12",
diff --git a/pb_migrations/1758049631_updated_player_stats.js b/pb_migrations/1758049631_updated_player_stats.js
new file mode 100644
index 0000000..ce9819c
--- /dev/null
+++ b/pb_migrations/1758049631_updated_player_stats.js
@@ -0,0 +1,37 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1358894712")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id"
+ }, collection)
+
+ // add field
+ collection.fields.addAt(4, new Field({
+ "hidden": false,
+ "id": "number3837590211",
+ "max": null,
+ "min": null,
+ "name": "tournaments",
+ "onlyInt": false,
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "number"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1358894712")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id"
+ }, collection)
+
+ // remove field
+ collection.fields.removeById("number3837590211")
+
+ return app.save(collection)
+})
diff --git a/pb_migrations/1758054877_updated_tournaments.js b/pb_migrations/1758054877_updated_tournaments.js
new file mode 100644
index 0000000..61e5924
--- /dev/null
+++ b/pb_migrations/1758054877_updated_tournaments.js
@@ -0,0 +1,28 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_340646327")
+
+ // add field
+ collection.fields.addAt(11, new Field({
+ "cascadeDelete": false,
+ "collectionId": "pbc_3072146508",
+ "hidden": false,
+ "id": "relation1584152981",
+ "maxSelect": 999,
+ "minSelect": 0,
+ "name": "free_agents",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_340646327")
+
+ // remove field
+ collection.fields.removeById("relation1584152981")
+
+ return app.save(collection)
+})
diff --git a/pb_migrations/1758081731_created_reactions.js b/pb_migrations/1758081731_created_reactions.js
new file mode 100644
index 0000000..a2e9597
--- /dev/null
+++ b/pb_migrations/1758081731_created_reactions.js
@@ -0,0 +1,84 @@
+///
+migrate((app) => {
+ const collection = new Collection({
+ "createRule": null,
+ "deleteRule": null,
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 15,
+ "min": 15,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3058431538",
+ "max": 0,
+ "min": 0,
+ "name": "emoji",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "cascadeDelete": false,
+ "collectionId": "pbc_3072146508",
+ "hidden": false,
+ "id": "relation2551806565",
+ "maxSelect": 1,
+ "minSelect": 0,
+ "name": "player",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ },
+ {
+ "hidden": false,
+ "id": "autodate2990389176",
+ "name": "created",
+ "onCreate": true,
+ "onUpdate": false,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ },
+ {
+ "hidden": false,
+ "id": "autodate3332085495",
+ "name": "updated",
+ "onCreate": true,
+ "onUpdate": true,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ }
+ ],
+ "id": "pbc_1549310251",
+ "indexes": [],
+ "listRule": null,
+ "name": "reactions",
+ "system": false,
+ "type": "base",
+ "updateRule": null,
+ "viewRule": null
+ });
+
+ return app.save(collection);
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_1549310251");
+
+ return app.delete(collection);
+})
diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx
index 0459715..1e69f6a 100644
--- a/src/app/routes/__root.tsx
+++ b/src/app/routes/__root.tsx
@@ -1,6 +1,7 @@
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/carousel/styles.css";
+import '@mantine/tiptap/styles.css';
import {
HeadContent,
Navigate,
diff --git a/src/app/routes/_authed/stats.tsx b/src/app/routes/_authed/stats.tsx
index cf498ee..a24cb0c 100644
--- a/src/app/routes/_authed/stats.tsx
+++ b/src/app/routes/_authed/stats.tsx
@@ -10,7 +10,7 @@ export const Route = createFileRoute("/_authed/stats")({
await ensureServerQueryData(queryClient, playerQueries.allStats());
},
loader: () => ({
- withPadding: true,
+ withPadding: false,
fullWidth: true,
header: {
title: "Player Stats"
diff --git a/src/app/routes/_authed/tournaments/index.tsx b/src/app/routes/_authed/tournaments/index.tsx
index 902ab64..05ca697 100644
--- a/src/app/routes/_authed/tournaments/index.tsx
+++ b/src/app/routes/_authed/tournaments/index.tsx
@@ -31,7 +31,7 @@ function RouteComponent() {
const { roles } = useAuth();
const sheet = useSheet();
- return
+ return (
{
roles?.includes("Admin") ? (
@@ -49,5 +49,5 @@ function RouteComponent() {
))
}
-
+ )
}
diff --git a/src/components/page.tsx b/src/components/page.tsx
index b1f037c..ec7d8a6 100644
--- a/src/components/page.tsx
+++ b/src/components/page.tsx
@@ -22,7 +22,7 @@ const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => {
{...props}
>
{header.collapsed && header.withBackButton && (
-
+
)}
{header.collapsed && header.settingsLink && (
diff --git a/src/components/rich-text-editor.tsx b/src/components/rich-text-editor.tsx
new file mode 100644
index 0000000..74fd872
--- /dev/null
+++ b/src/components/rich-text-editor.tsx
@@ -0,0 +1,40 @@
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import { RichTextEditor as MantineRichTextEditor } from '@mantine/tiptap';
+
+interface RichTextEditorProps {
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export function RichTextEditor({
+ value,
+ onChange,
+}: RichTextEditorProps) {
+ const editor = useEditor({
+ extensions: [StarterKit],
+ content: value,
+ onUpdate: ({ editor }) => {
+ onChange(editor.getHTML());
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx
index e5c705b..d64edc0 100644
--- a/src/components/swipeable-tabs.tsx
+++ b/src/components/swipeable-tabs.tsx
@@ -98,13 +98,11 @@ function SwipeableTabs({
updateHeight();
}, [activeTab, updateHeight]);
- // Update height when content changes (after render)
useEffect(() => {
const timeoutId = setTimeout(updateHeight, 0);
return () => clearTimeout(timeoutId);
});
- // Use ResizeObserver to watch for content size changes
useEffect(() => {
const activeSlideRef = slideRefs.current[activeTab];
if (!activeSlideRef) return;
@@ -142,6 +140,7 @@ function SwipeableTabs({
top={0}
style={{
display: "flex",
+ paddingInline: "var(--mantine-spacing-md)",
marginBottom: "var(--mantine-spacing-md)",
zIndex: 100,
backgroundColor: "var(--mantine-color-body)",
diff --git a/src/features/core/components/back-button.tsx b/src/features/core/components/back-button.tsx
index 863d543..86c1cb8 100644
--- a/src/features/core/components/back-button.tsx
+++ b/src/features/core/components/back-button.tsx
@@ -2,7 +2,7 @@ import { Box } from "@mantine/core"
import { ArrowLeftIcon } from "@phosphor-icons/react"
import { useRouter } from "@tanstack/react-router"
-const BackButton = () => {
+const BackButton = ({ top=20, left=20 }: { top?: number, left?: number }) => {
const router = useRouter()
return (
@@ -10,8 +10,8 @@ const BackButton = () => {
style={{ cursor: 'pointer', zIndex: 1000 }}
onClick={() => router.history.back()}
pos='absolute'
- left={16}
- top={0}
+ left={left}
+ top={top}
>
diff --git a/src/features/core/components/header.tsx b/src/features/core/components/header.tsx
index 2c75931..dfb0d82 100644
--- a/src/features/core/components/header.tsx
+++ b/src/features/core/components/header.tsx
@@ -1,12 +1,16 @@
import { Title, AppShell, Flex } from "@mantine/core";
import { HeaderConfig } from "../types/header-config";
+import useRouterConfig from "../hooks/use-router-config";
+import BackButton from "./back-button";
interface HeaderProps extends HeaderConfig {}
const Header = ({ collapsed, title }: HeaderProps) => {
+ const { header } = useRouterConfig();
return (