Merge pull request #34 from atridadl/dev

1.2.7
 Shiny new stats on the front page
🚧 Small cleanup of react FC references...
This commit is contained in:
Atridad Lahiji 2023-08-08 22:41:56 -06:00 committed by GitHub
commit ad0be32e52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 163 additions and 95 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "sprintpadawan", "name": "sprintpadawan",
"version": "1.2.6", "version": "1.2.7",
"description": "Plan. Sprint. Repeat.", "description": "Plan. Sprint. Repeat.",
"private": true, "private": true,
"scripts": { "scripts": {

View file

@ -1,14 +1,14 @@
import { GiTechnoHeart } from "react-icons/gi"; import { GiTechnoHeart } from "react-icons/gi";
import packagejson from "../../package.json"; import packagejson from "../../package.json";
const Footer: React.FC = () => { const Footer = () => {
return ( return (
<footer className="footer footer-center h-12 p-2 bg-base-100 text-base-content"> <footer className="footer footer-center h-12 p-2 bg-base-100 text-base-content">
<div> <div>
<p> <p>
Made with{ " " } Made with{" "}
<GiTechnoHeart className="inline-block text-primary text-lg animate-pulse" />{ " " } <GiTechnoHeart className="inline-block text-primary text-lg animate-pulse" />{" "}
by{ " " } by{" "}
<a <a
className="link link-primary link-hover" className="link link-primary link-hover"
href="https://atri.dad" href="https://atri.dad"
@ -16,15 +16,15 @@ const Footer: React.FC = () => {
target="_blank" target="_blank"
> >
Atridad Lahiji Atridad Lahiji
</a>{ " " } </a>{" "}
-{ " " } -{" "}
<a <a
className="link link-primary link-hover" className="link link-primary link-hover"
href={ `https://github.com/atridadl/sprintpadawan/releases/tag/${packagejson.version}` } href={`https://github.com/atridadl/sprintpadawan/releases/tag/${packagejson.version}`}
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
v{ packagejson.version } v{packagejson.version}
</a> </a>
</p> </p>
</div> </div>

View file

@ -8,7 +8,7 @@ interface NavbarProps {
title: string; title: string;
} }
const Navbar: React.FC<NavbarProps> = ({ title }) => { const Navbar = ({ title }: NavbarProps) => {
const { data: sessionData, status: sessionStatus } = useSession(); const { data: sessionData, status: sessionStatus } = useSession();
const router = useRouter(); const router = useRouter();

View file

@ -7,7 +7,7 @@ import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
const RoomList: React.FC = () => { const RoomList = () => {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
configureAbly({ configureAbly({
@ -50,7 +50,7 @@ const RoomList: React.FC = () => {
return ( return (
<div className="flex flex-col items-center justify-center gap-8"> <div className="flex flex-col items-center justify-center gap-8">
{/* Modal for Adding Rooms */ } {/* Modal for Adding Rooms */}
<input type="checkbox" id="new-room-modal" className="modal-toggle" /> <input type="checkbox" id="new-room-modal" className="modal-toggle" />
<div className="modal modal-bottom sm:modal-middle"> <div className="modal modal-bottom sm:modal-middle">
<div className="modal-box flex-col flex text-center justify-center items-center"> <div className="modal-box flex-col flex text-center justify-center items-center">
@ -72,30 +72,30 @@ const RoomList: React.FC = () => {
type="text" type="text"
placeholder="Type here" placeholder="Type here"
className="input input-bordered w-full max-w-xs" className="input input-bordered w-full max-w-xs"
onChange={ (event) => { onChange={(event) => {
setRoomName(event.target.value); setRoomName(event.target.value);
} } }}
/> />
</div> </div>
<div className="modal-action"> <div className="modal-action">
{ roomName.length > 0 && ( {roomName.length > 0 && (
<label <label
htmlFor="new-room-modal" htmlFor="new-room-modal"
className="btn btn-primary" className="btn btn-primary"
onClick={ () => createRoomHandler() } onClick={() => createRoomHandler()}
> >
Submit Submit
</label> </label>
) } )}
</div> </div>
</div> </div>
</div> </div>
{ roomsFromDb && roomsFromDb.length > 0 && ( {roomsFromDb && roomsFromDb.length > 0 && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="table text-center"> <table className="table text-center">
{/* head */ } {/* head */}
<thead> <thead>
<tr className="border-white"> <tr className="border-white">
<th>Room Name</th> <th>Room Name</th>
@ -103,43 +103,43 @@ const RoomList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className=""> <tbody className="">
{ roomsFromDb?.map((room) => { {roomsFromDb?.map((room) => {
return ( return (
<tr key={ room.id } className="hover border-white"> <tr key={room.id} className="hover border-white">
<td className="break-all max-w-[200px] md:max-w-[400px]"> <td className="break-all max-w-[200px] md:max-w-[400px]">
{ room.roomName } {room.roomName}
</td> </td>
<td> <td>
<Link <Link
className="m-2 no-underline" className="m-2 no-underline"
href={ `/room/${room.id}` } href={`/room/${room.id}`}
> >
<IoEnterOutline className="text-xl inline-block hover:text-secondary" /> <IoEnterOutline className="text-xl inline-block hover:text-secondary" />
</Link> </Link>
<button <button
className="m-2" className="m-2"
onClick={ () => deleteRoomHandler(room.id) } onClick={() => deleteRoomHandler(room.id)}
> >
<IoTrashBinOutline className="text-xl inline-block hover:text-error" /> <IoTrashBinOutline className="text-xl inline-block hover:text-error" />
</button> </button>
</td> </td>
</tr> </tr>
); );
}) } })}
</tbody> </tbody>
</table> </table>
</div> </div>
) } )}
<label htmlFor="new-room-modal" className="btn btn-secondary"> <label htmlFor="new-room-modal" className="btn btn-secondary">
New Room New Room
</label> </label>
{ roomsFromDb === undefined && ( {roomsFromDb === undefined && (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span className="loading loading-dots loading-lg"></span> <span className="loading loading-dots loading-lg"></span>
</div> </div>
) } )}
</div> </div>
); );
}; };

83
src/components/Stats.tsx Normal file
View file

@ -0,0 +1,83 @@
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
import { env } from "~/env.mjs";
import { api } from "~/utils/api";
const Stats = () => {
configureAbly({
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
recover: (_, cb) => {
cb(true);
},
});
const [] = useChannel(
`${env.NEXT_PUBLIC_APP_ENV}-stats`,
() => void refetchData()
);
const {
data: usersCount,
isLoading: usersCountLoading,
isFetching: usersCountFetching,
refetch: refetchUsersCount,
} = api.rest.userCount.useQuery();
const {
data: roomsCount,
isLoading: roomsCountLoading,
isFetching: roomsCountFetching,
refetch: refetchRoomsCount,
} = api.rest.roomCount.useQuery();
const {
data: votesCount,
isLoading: votesCountLoading,
isFetching: votesCountFetching,
refetch: refetchVotesCount,
} = api.rest.voteCount.useQuery();
const refetchData = async () => {
await Promise.all([
refetchUsersCount(),
refetchRoomsCount(),
refetchVotesCount(),
]);
};
return (
<div className="stats stats-horizontal shadow bg-neutral m-4">
<div className="stat">
<div className="stat-title">Users</div>
<div className="stat-value">
{usersCountLoading || usersCountFetching ? (
<span className="loading loading-infinity loading-lg"></span>
) : (
<>{usersCount ? usersCount : "0"}</>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">Rooms</div>
<div className="stat-value">
{roomsCountLoading || roomsCountFetching ? (
<span className="loading loading-infinity loading-lg"></span>
) : (
<>{roomsCount ? roomsCount : "0"}</>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">Votes</div>
<div className="stat-value">
{votesCountLoading || votesCountFetching ? (
<span className="loading loading-infinity loading-lg"></span>
) : (
<>{votesCount ? votesCount : "0"}</>
)}
</div>
</div>
</div>
);
};
export default Stats;

View file

@ -8,6 +8,7 @@ import { SiGithub, SiGoogle } from "react-icons/si";
import { GiStarFormation } from "react-icons/gi"; import { GiStarFormation } from "react-icons/gi";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { getServerAuthSession } from "../../server/auth"; import { getServerAuthSession } from "../../server/auth";
import Stats from "~/components/Stats";
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx); const session = await getServerAuthSession(ctx);
@ -55,31 +56,13 @@ const Admin: NextPage = () => {
export default Admin; export default Admin;
const AdminBody: React.FC = () => { const AdminBody = () => {
const {
data: usersCount,
isLoading: usersCountLoading,
isFetching: usersCountFetching,
refetch: refetchUsersCount,
} = api.rest.userCount.useQuery();
const { const {
data: users, data: users,
isLoading: usersLoading, isLoading: usersLoading,
isFetching: usersFetching, isFetching: usersFetching,
refetch: refetchUsers, refetch: refetchUsers,
} = api.user.getAll.useQuery(); } = api.user.getAll.useQuery();
const {
data: roomsCount,
isLoading: roomsCountLoading,
isFetching: roomsCountFetching,
refetch: refetchRoomsCount,
} = api.rest.roomCount.useQuery();
const {
data: votesCount,
isLoading: votesCountLoading,
isFetching: votesCountFetching,
refetch: refetchVotesCount,
} = api.rest.voteCount.useQuery();
const getProviders = (user: { const getProviders = (user: {
createdAt: Date; createdAt: Date;
@ -153,57 +136,16 @@ const AdminBody: React.FC = () => {
}; };
const refetchData = async () => { const refetchData = async () => {
await Promise.all([ await Promise.all([refetchUsers()]);
refetchUsers(),
refetchUsersCount(),
refetchRoomsCount(),
refetchVotesCount(),
]);
}; };
return ( return (
<> <>
<h1 className="text-4xl font-bold">Admin Panel</h1> <h1 className="text-4xl font-bold">Admin Panel</h1>
<div className="stats stats-horizontal shadow bg-neutral m-4"> <Stats />
<div className="stat">
<div className="stat-title">Users</div>
<div className="stat-value">
{usersCountLoading || usersCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{usersCount ? usersCount : "0"}</>
)}
</div>
</div>
<div className="stat"> {usersFetching ? (
<div className="stat-title">Rooms</div>
<div className="stat-value">
{roomsCountLoading || roomsCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{roomsCount ? roomsCount : "0"}</>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">Votes</div>
<div className="stat-value">
{votesCountLoading || votesCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{votesCount ? votesCount : "0"}</>
)}
</div>
</div>
</div>
{usersCountFetching ||
usersFetching ||
roomsCountFetching ||
votesCountFetching ? (
<span className="loading loading-dots loading-lg"></span> <span className="loading loading-dots loading-lg"></span>
) : ( ) : (
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2"> <div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">

View file

@ -45,7 +45,7 @@ const Home: NextPage = () => {
export default Home; export default Home;
const HomePageBody: React.FC = () => { const HomePageBody = () => {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>(""); const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
const [tabIndex, setTabIndex] = useState<number>(); const [tabIndex, setTabIndex] = useState<number>();

View file

@ -1,5 +1,6 @@
import { type NextPage } from "next"; import { type NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import Stats from "~/components/Stats";
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
@ -17,7 +18,7 @@ const Home: NextPage = () => {
export default Home; export default Home;
const HomePageBody: React.FC = () => { const HomePageBody = () => {
return ( return (
<> <>
<h1 className="text-3xl sm:text-6xl font-bold"> <h1 className="text-3xl sm:text-6xl font-bold">
@ -54,6 +55,13 @@ const HomePageBody: React.FC = () => {
</ul> </ul>
</div> </div>
</div> </div>
<div className="card card-compact bg-secondary text-black font-bold text-left">
<div className="card-body">
<h2 className="card-title">Stats:</h2>
<Stats />
</div>
</div>
</> </>
); );
}; };

View file

@ -48,7 +48,7 @@ const Profile: NextPage = () => {
export default Profile; export default Profile;
const ProfileBody: React.FC = () => { const ProfileBody = () => {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const [nameText, setNameText] = useState<string>(""); const [nameText, setNameText] = useState<string>("");
const router = useRouter(); const router = useRouter();

View file

@ -65,7 +65,7 @@ const Room: NextPage = () => {
export default Room; export default Room;
const RoomBody: React.FC = ({}) => { const RoomBody = ({}) => {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const { query } = useRouter(); const { query } = useRouter();
const roomId = z.string().parse(query.id); const roomId = z.string().parse(query.id);

View file

@ -33,6 +33,12 @@ export const roomRouter = createTRPCRouter({
EventTypes.ROOM_LIST_UPDATE, EventTypes.ROOM_LIST_UPDATE,
JSON.stringify(room) JSON.stringify(room)
); );
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(room)
);
} }
// happy path // happy path
return !!room; return !!room;
@ -224,6 +230,12 @@ export const roomRouter = createTRPCRouter({
EventTypes.ROOM_UPDATE, EventTypes.ROOM_UPDATE,
JSON.stringify(deletedRoom) JSON.stringify(deletedRoom)
); );
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(deletedRoom)
);
} }
return !!deletedRoom; return !!deletedRoom;

View file

@ -3,6 +3,7 @@ import { Resend } from "resend";
import { z } from "zod"; import { z } from "zod";
import { Goodbye } from "~/components/templates/Goodbye"; import { Goodbye } from "~/components/templates/Goodbye";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { publishToChannel } from "~/server/ably";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
@ -10,6 +11,7 @@ import {
} from "~/server/api/trpc"; } from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
import { EventTypes } from "~/utils/types";
const resend = new Resend(process.env.RESEND_API_KEY); const resend = new Resend(process.env.RESEND_API_KEY);
@ -118,6 +120,12 @@ export const userRouter = createTRPCRouter({
await invalidateCache(`kv_usercount_admin`); await invalidateCache(`kv_usercount_admin`);
await invalidateCache(`kv_userlist_admin`); await invalidateCache(`kv_userlist_admin`);
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(user)
);
} }
return !!user; return !!user;

View file

@ -93,6 +93,12 @@ export const voteRouter = createTRPCRouter({
EventTypes.VOTE_UPDATE, EventTypes.VOTE_UPDATE,
input.value input.value
); );
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(vote)
);
} }
return !!vote; return !!vote;

View file

@ -12,6 +12,8 @@ import { env } from "~/env.mjs";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { Welcome } from "../components/templates/Welcome"; import { Welcome } from "../components/templates/Welcome";
import { invalidateCache } from "./redis"; import { invalidateCache } from "./redis";
import { publishToChannel } from "./ably";
import { EventTypes } from "~/utils/types";
const resend = new Resend(process.env.RESEND_API_KEY); const resend = new Resend(process.env.RESEND_API_KEY);
@ -65,6 +67,12 @@ export const authOptions: NextAuthOptions = {
}); });
await invalidateCache(`kv_userlist_admin`); await invalidateCache(`kv_userlist_admin`);
await invalidateCache(`kv_usercount_admin`); await invalidateCache(`kv_usercount_admin`);
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(user)
);
} }
}, },
async signIn({}) { async signIn({}) {

View file

@ -4,6 +4,7 @@ export const EventTypes = {
ROOM_LIST_UPDATE: "room.list.update", ROOM_LIST_UPDATE: "room.list.update",
ROOM_UPDATE: "room.update", ROOM_UPDATE: "room.update",
VOTE_UPDATE: "vote.update", VOTE_UPDATE: "vote.update",
STATS_UPDATE: "stats.update",
} as const; } as const;
export type EventType = BetterEnum<typeof EventTypes>; export type EventType = BetterEnum<typeof EventTypes>;