Skip to content

Commit

Permalink
Merge branch 'home-view-update'
Browse files Browse the repository at this point in the history
  • Loading branch information
sofvanh committed Jan 10, 2025
2 parents e057350 + aa1c3b6 commit 4883f19
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 53 deletions.
10 changes: 10 additions & 0 deletions backend/src/db/getTimestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function getTimestamp(id: string): Date {
const parts = id.split('_');
if (parts.length !== 2) {
throw new Error('Invalid ID format');
}

const timestampBase36 = parts[1].slice(0, -6);
const timestamp = parseInt(timestampBase36, 36);
return new Date(timestamp);
}
75 changes: 68 additions & 7 deletions backend/src/db/operations/graphOperations.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { query } from '../db';
import { generateGraphId } from '../idGenerator';
import { Argument, Edge, Graph } from '../../.shared/types';
import { Argument, Edge, Graph, GraphData } from '../../.shared/types';
import { getArgumentScores } from '../../analysis/argumentScoreHandler';
import { getReactionCounts } from './reactionOperations';
import { getTimestamp } from '../getTimestamp';

export async function createGraph(name: string, authorId: string): Promise<string> {
const id = generateGraphId();
Expand All @@ -18,7 +19,67 @@ export async function getGraphs(): Promise<{ id: string; name: string }[]> {
return result.rows;
}

export async function getGraphData(graphId: string): Promise<Graph> {
export async function getFeaturedGraphs(): Promise<GraphData[]> {
const FEATURED_GRAPH_IDS = [
"gra_m47bz12vUA7fMZ",
"gra_m4a9lakjDPG7vU",
"gra_m4abp2spuJ9yW5"
]
const graphs = await Promise.all(FEATURED_GRAPH_IDS.map(id => getGraphData(id)));
return graphs;
}

export async function getUserGraphs(userId: string): Promise<GraphData[]> {
const graphIdsResult = await query(
`SELECT DISTINCT g.id
FROM graphs g
LEFT JOIN arguments a ON g.id = a.graph_id
LEFT JOIN reactions r ON g.id = (
SELECT graph_id
FROM arguments
WHERE id = r.argument_id
)
WHERE g.author_id = $1
OR a.author_id = $1
OR r.user_id = $1`,
[userId]
);

const graphs: GraphData[] = [];
for (const row of graphIdsResult.rows) {
const graphData = await getGraphData(row.id);
graphs.push(graphData);
}

return graphs;
}

export async function getGraphData(graphId: string): Promise<GraphData> {
const graphResult = await query('SELECT * FROM graphs WHERE id = $1', [graphId]);
if (graphResult.rows.length === 0) {
throw new Error('Graph not found');
}

const statsResult = await query(
`SELECT
COUNT(*) as argument_count,
MAX(id) as latest_argument_id
FROM arguments
WHERE graph_id = $1`,
[graphId]
);

const lastActivity = statsResult.rows[0].latest_argument_id ? getTimestamp(statsResult.rows[0].latest_argument_id) : undefined;

return {
id: graphResult.rows[0].id,
name: graphResult.rows[0].name,
argumentCount: parseInt(statsResult.rows[0].argument_count),
lastActivity: lastActivity
};
}

export async function getFullGraph(graphId: string): Promise<Graph> {
const graphResult = await query('SELECT * FROM graphs WHERE id = $1', [graphId]);
if (graphResult.rows.length === 0) {
throw new Error('Graph not found');
Expand Down Expand Up @@ -55,8 +116,8 @@ export async function getGraphData(graphId: string): Promise<Graph> {
} as Graph;
}

export async function getGraphDataWithUserReactions(graphId: string, userId: string): Promise<Graph> {
const graphData = await getGraphData(graphId);
export async function getFullGraphWithUserReactions(graphId: string, userId: string): Promise<Graph> {
const graph = await getFullGraph(graphId);
let userReactionsMap = new Map();
const userReactionsResult = await query(
`SELECT argument_id, type
Expand All @@ -71,15 +132,15 @@ export async function getGraphDataWithUserReactions(graphId: string, userId: str
userReactionsMap.get(row.argument_id)[row.type] = true;
});

const argsWithUserReactions = graphData.arguments.map(arg => ({
const argsWithUserReactions = graph.arguments.map(arg => ({
...arg,
userReaction: userReactionsMap.get(arg.id) || {} // TODO I also want typing of the user reaction
}));

return {
id: graphId,
name: graphData.name,
name: graph.name,
arguments: argsWithUserReactions,
edges: graphData.edges
edges: graph.edges
} as Graph;
}
4 changes: 4 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { handleAuthenticate } from './websocket/auth/authenticate';
import { handleLogout } from './websocket/auth/logout';
import { SocketHandler, SocketResponse } from './backendTypes';
import { handleGetGraphs } from './websocket/graph/getGraphs';
import { handleGetFeaturedGraphs } from './websocket/graph/getFeaturedGraphs';
import { handleCreateGraph } from './websocket/graph/createGraph';
import { handleJoinGraph } from './websocket/graph/joinGraph';
import { handleLeaveGraph } from './websocket/graph/leaveGraph';
import { handleAddArgument } from './websocket/argument/addArgument';
import { handleAddReaction } from './websocket/reaction/addReaction';
import { handleRemoveReaction } from './websocket/reaction/removeReaction';
import { handleGetMyGraphs } from './websocket/graph/getMyGraphs';

const app = express();
const server = http.createServer(app);
Expand Down Expand Up @@ -59,6 +61,8 @@ io.on('connection', (socket) => {
socket.on('authenticate', wrapHandler(handleAuthenticate));
socket.on('logout', wrapHandler(handleLogout));
socket.on('get graphs', wrapHandler(handleGetGraphs));
socket.on('get featured graphs', wrapHandler(handleGetFeaturedGraphs));
socket.on('get my graphs', wrapHandler(handleGetMyGraphs));
socket.on('create graph', wrapHandler(handleCreateGraph));
socket.on('join graph', wrapHandler(handleJoinGraph));
socket.on('leave graph', wrapHandler(handleLeaveGraph));
Expand Down
4 changes: 2 additions & 2 deletions backend/src/websocket/argument/addArgument.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SocketHandler } from "../../backendTypes";
import { generateTopKSimilarEdges } from "../../embeddingHandler";
import { addArgument } from "../../db/operations/argumentOperations";
import { getGraphDataWithUserReactions } from "../../db/operations/graphOperations";
import { getFullGraphWithUserReactions } from "../../db/operations/graphOperations";
import { embedText } from "../../embeddingHandler";
import { sendNewArgumentUpdate } from "../updateHandler";
import { updateGraphEdges } from "../../db/operations/edgeOperations";
Expand All @@ -16,7 +16,7 @@ export const handleAddArgument: SocketHandler<AddArgumentData, {}> = async (sock
return { success: false, error: 'Authentication required' };
}

const graph = await getGraphDataWithUserReactions(graphId, socket.data.user.id);
const graph = await getFullGraphWithUserReactions(graphId, socket.data.user.id);
if (!graph) {
return { success: false, error: 'Graph not found' };
}
Expand Down
15 changes: 15 additions & 0 deletions backend/src/websocket/graph/getFeaturedGraphs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SocketHandler } from "../../backendTypes";
import { getFeaturedGraphs } from "../../db/operations/graphOperations";
import { GraphData } from "../../.shared/types";

interface GetFeaturedGraphsResponse {
graphs: GraphData[]
}

export const handleGetFeaturedGraphs: SocketHandler<{}, GetFeaturedGraphsResponse> = async (socket, io, { }) => {
const graphs = await getFeaturedGraphs();
return {
success: true,
data: { graphs }
};
}
21 changes: 21 additions & 0 deletions backend/src/websocket/graph/getMyGraphs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SocketHandler } from "../../backendTypes";
import { getUserGraphs } from "../../db/operations/graphOperations";
import { GraphData } from "../../.shared/types";

interface GetMyGraphsResponse {
graphs: GraphData[]
}

export const handleGetMyGraphs: SocketHandler<{}, GetMyGraphsResponse> = async (socket, io, { }) => {
if (!socket.data.user) {
return {
success: false,
error: 'Authentication required'
};
}
const graphs = await getUserGraphs(socket.data.user.id);
return {
success: true,
data: { graphs }
};
}
4 changes: 2 additions & 2 deletions backend/src/websocket/graph/joinGraph.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Graph } from "../../.shared/types";
import { SocketHandler } from "../../backendTypes";
import { getGraphDataWithUserReactions } from "../../db/operations/graphOperations";
import { getFullGraphWithUserReactions } from "../../db/operations/graphOperations";

interface JoinGraphData {
graphId: string;
Expand All @@ -13,7 +13,7 @@ interface JoinGraphResponse {
export const handleJoinGraph: SocketHandler<JoinGraphData, JoinGraphResponse> = async (socket, io, { graphId }) => {
socket.join(graphId);
console.log(`Socket ${socket.id} joining graph ${graphId}`);
const graph = await getGraphDataWithUserReactions(graphId, socket.data.user?.id);
const graph = await getFullGraphWithUserReactions(graphId, socket.data.user?.id);
return {
success: true,
data: { graph }
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/FeaturedGraphsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useEffect } from 'react';
import { useWebSocket } from '../contexts/WebSocketContext';
import { GraphData } from '../shared/types';
import { GraphsList } from './GraphsList';


export const FeaturedGraphsList = () => {
const { socket } = useWebSocket();
const [graphs, setGraphs] = useState<GraphData[]>([]);

useEffect(() => {
socket?.emit('get featured graphs', {}, (response: any) => {
if (response.success) {
setGraphs(response.data.graphs);
}
});
}, [socket]);

return (
<div className="flex flex-col mx-auto my-4">
<h2>Featured graphs</h2>
<GraphsList graphs={graphs} />
</div>
);
};
5 changes: 1 addition & 4 deletions frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ export default function Footer() {
<footer className="bg-white border-t border-stone-200 px-4 text-center h-16 flex items-center justify-center text-sm text-stone-400">
<div className="flex justify-between w-full">
<p className="text-stone-400">
Crafted by
<span className="hover:text-stone-600"> Niki</span> {/* TODO Add Niki's website once it exists */}
and&nbsp;
<a href="https://sofiavanhanen.fi" target="_blank" rel="noopener noreferrer">Sofi</a>
Crafted by <a href="https://mosaic-labs.org" target="_blank" rel="noopener noreferrer">Mosaic Labs</a>
</p>
<a href="https://github.com/sofvanh/mindmeld" target="_blank" rel="noopener noreferrer" className="inline-flex items-center">
<FaGithub className="w-4 h-4 mr-1" />
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/GraphInfoBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Link } from 'react-router-dom';
import { GraphData } from '../shared/types';
import { formatDate } from '../utils/time';

export const GraphInfoBox = ({ id, name, argumentCount, lastActivity }: GraphData) => {
return (
<Link to={`/graph/${id}`} className="block">
<div className="py-4 bg-white hover:bg-stone-50 transition-colors duration-100 text-stone-700 border-t border-stone-200">
<h4 className="m-0 text-base">{name}</h4>
<div className="text-sm text-stone-500 flex justify-between">
<span>
{argumentCount} argument{argumentCount !== 1 ? 's' : ''}
</span>
<span>
Last activity: {lastActivity ? formatDate(lastActivity) : 'never'}
</span>
</div>
</div>
</Link>
);
};
23 changes: 23 additions & 0 deletions frontend/src/components/GraphsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { GraphData } from '../shared/types';
import { GraphInfoBox } from './GraphInfoBox';
import LoadingSpinner from './LoadingSpinner';

interface GraphsListProps {
graphs: GraphData[];
}

export const GraphsList = ({ graphs }: GraphsListProps) => {
return (
<div className="flex flex-col mx-auto w-full">
{graphs.length > 0 ? (
<div className="border-b border-stone-200">
{graphs.map(graph => (
<GraphInfoBox key={graph.id} {...graph} />
))}
</div>
) : (
<LoadingSpinner className="mt-4" />
)}
</div>
);
};
25 changes: 25 additions & 0 deletions frontend/src/components/MyGraphsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useEffect } from 'react';
import { useWebSocket } from '../contexts/WebSocketContext';
import { GraphData } from '../shared/types';
import { GraphsList } from './GraphsList';


export const MyGraphsList = () => {
const { socket } = useWebSocket();
const [graphs, setGraphs] = useState<GraphData[]>([]);

useEffect(() => {
socket?.emit('get my graphs', {}, (response: any) => {
if (response.success) {
setGraphs(response.data.graphs);
}
});
}, [socket]);

return (
<div className="flex flex-col mx-auto mt-4">
<h2>My graphs</h2>
<GraphsList graphs={graphs} />
</div>
);
};
16 changes: 12 additions & 4 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
}

h1 {
@apply font-bricolage mb-4 text-4xl font-semibold;
@apply font-bricolage mb-4 text-4xl font-bold text-stone-700;
}

h2 {
@apply font-system mb-4 text-2xl font-semibold;
@apply font-system mb-4 text-2xl font-semibold text-stone-700;
}

h3 {
@apply font-system mb-4 text-xl font-semibold;
}

h4 {
@apply font-system mb-4 text-lg font-medium;
}

a {
@apply text-sky-500 rounded hover:text-sky-700 transition-colors duration-200;
}

p {
@apply text-stone-700;
@apply text-stone-700 my-4;
}

.card {
Expand All @@ -33,7 +37,11 @@

input,
textarea {
@apply px-3 py-2 border border-stone-200 rounded text-stone-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent text-sm;
@apply px-3 py-2 border border-stone-200 rounded text-stone-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent text-sm;
}

input {
@apply w-full;
}

button {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export interface Edge {
targetId: string;
}

export interface GraphData {
id: string;
name: string;
argumentCount: number;
lastActivity: number;
}

export interface Graph {
id: string;
name: string;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/styles/defaultStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const amberButtonColors = "text-amber-500 hover:text-amber-600 hover:bg-amber-50
const baseIconButtonClasses = `p-0 w-6 h-6 flex items-center justify-center`;

export const buttonStyles = {
primary: `bg-sky-500 hover:bg-sky-600 text-white py-2 px-4`,
primary: `bg-emerald-500 hover:bg-emerald-600 text-white py-2 px-4`,
secondary: `${secondaryButtonColors} py-2 px-4`,
green: `${greenButtonColors} py-2 px-4`,
red: `${redButtonColors} py-2 px-4`,
Expand Down
Loading

0 comments on commit 4883f19

Please sign in to comment.