First commit

This commit is contained in:
srosset81
2026-07-10 11:45:54 +02:00
commit 30ea3db684
37 changed files with 10866 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
import {
Refine,
Authenticated,
} from "@refinedev/core";
import {
useNotificationProvider,
ThemedLayout,
ErrorComponent,
RefineThemes,
} from "@refinedev/antd";
import {
DashboardOutlined,
} from "@ant-design/icons";
import routerProvider, {
NavigateToResource,
CatchAllNavigate,
UnsavedChangesNotifier,
DocumentTitleHandler,
} from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router";
import { App as AntdApp, ConfigProvider } from "antd";
import "@ant-design/v5-patch-for-react-19";
import "@refinedev/antd/dist/reset.css";
import { authProvider, dataProvider, liveProvider } from "@ng-org/refine-providers";
import dataModels from "./shapes/data-models";
import AuthPage from "./components/AuthPage";
import { PostList, PostCreate, PostEdit, PostShow } from "../src/pages/posts";
import { DashboardPage } from "../src/pages/dashboard";
import { getSession } from "./utils/initNg";
const App: React.FC = () => (
<BrowserRouter basename={import.meta.env.MODE === 'production' ? '/nextgraph-refine-app/' : '/'}>
<ConfigProvider theme={RefineThemes.Blue}>
<AntdApp>
<Refine
// authProvider={authProvider}
dataProvider={dataProvider({ dataModels, getSession })}
liveProvider={liveProvider({ dataModels })}
routerProvider={routerProvider}
resources={[
// {
// name: "dashboard",
// list: "/",
// meta: {
// label: "Dashboard",
// icon: <DashboardOutlined />,
// },
// },
{
name: "posts",
list: "/posts",
create: "/posts/create",
show: "/posts/show/:id",
edit: "/posts/edit/:id",
meta: {
canDelete: true,
},
},
]}
notificationProvider={useNotificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
liveMode: "auto",
disableTelemetry: true,
}}
>
<Routes>
<Route
element={
// <Authenticated
// key="authenticated-routes"
// fallback={<CatchAllNavigate to="/login" />}
// >
<ThemedLayout>
<Outlet />
</ThemedLayout>
// </Authenticated>
}
>
<Route index element={<Navigate to="/" />} />
{/* <Route index element={<DashboardPage />} /> */}
<Route path="/posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="edit/:id" element={<PostEdit />} />
<Route path="show/:id" element={<PostShow />} />
</Route>
</Route>
{/* <Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<NavigateToResource resource="posts" />
</Authenticated>
}
>
<Route path="/login" element={<AuthPage />} />
<Route path="/register" element={<AuthPage />} />
</Route> */}
<Route
element={
// <Authenticated key="catch-all">
<ThemedLayout>
<Outlet />
</ThemedLayout>
// </Authenticated>
}
>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</AntdApp>
</ConfigProvider>
</BrowserRouter>
);
export default App;
@@ -0,0 +1,172 @@
import type { CSSProperties } from "react";
import {
type LoginPageProps,
type LoginFormTypes,
} from "@refinedev/core";
import {
Row,
Col,
Layout,
Card,
Form,
Input,
Button,
type CardProps,
type LayoutProps,
type FormProps,
theme,
} from "antd";
import { useLogin, useTranslate } from "@refinedev/core";
import { ThemedTitle } from "@refinedev/antd";
export const layoutStyles: CSSProperties = {};
export const containerStyles: CSSProperties = {
maxWidth: "400px",
margin: "auto",
padding: "32px",
boxShadow:
"0px 2px 4px rgba(0, 0, 0, 0.02), 0px 1px 6px -1px rgba(0, 0, 0, 0.02), 0px 1px 2px rgba(0, 0, 0, 0.03)",
};
export const headStyles: CSSProperties = {
borderBottom: 0,
padding: 0,
};
export const bodyStyles: CSSProperties = { padding: 0 };
export const titleStyles: CSSProperties = {
textAlign: "center",
marginBottom: 0,
fontSize: "24px",
lineHeight: "32px",
fontWeight: 700,
overflowWrap: "break-word",
hyphens: "manual",
textOverflow: "unset",
whiteSpace: "pre-wrap",
};
type LoginProps = LoginPageProps<LayoutProps, CardProps, FormProps>;
export const AuthPage: React.FC<LoginProps> = ({
contentProps,
wrapperProps,
renderContent,
formProps,
title,
hideForm,
mutationVariables,
}) => {
const { token } = theme.useToken();
const [form] = Form.useForm<LoginFormTypes>();
const translate = useTranslate();
const { mutate: login, isPending } = useLogin<LoginFormTypes>();
const PageTitle =
title === false ? null : (
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "32px",
fontSize: "20px",
}}
>
{title ?? <ThemedTitle collapsed={false} />}
</div>
);
const CardContent = (
<Card
// title={CardTitle}
styles={{
header: headStyles,
body: bodyStyles,
}}
style={{
...containerStyles,
backgroundColor: token.colorBgElevated,
}}
{...(contentProps ?? {})}
>
<Form<LoginFormTypes>
layout="vertical"
form={form}
onFinish={(values) => login({ ...values, ...mutationVariables })}
requiredMark={false}
initialValues={{
brokerUrl: 'https://nextgraph.eu'
}}
{...formProps}
>
<Form.Item
name="brokerUrl"
label={translate("pages.login.fields.brokerUrl", "Broker URL")}
rules={[
{
required: true,
message: translate(
"pages.login.errors.requiredBrokerUrl",
"Broker URL is required",
),
},
{
type: "url",
message: translate(
"pages.login.errors.validBrokerUrl",
"Invalid broker URL",
),
},
]}
>
<Input
size="large"
placeholder={translate("pages.login.fields.brokerUrl", "Broker URL")}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
size="large"
htmlType="submit"
loading={isPending}
block
>
{translate("pages.login.signin", "Sign in")}
</Button>
</Form.Item>
</Form>
</Card>
);
return (
<Layout style={layoutStyles} {...(wrapperProps ?? {})}>
<Row
justify="center"
align={hideForm ? "top" : "middle"}
style={{
padding: "16px 0",
minHeight: "100dvh",
paddingTop: hideForm ? "15dvh" : "16px",
}}
>
<Col xs={22}>
{renderContent ? (
renderContent(CardContent, PageTitle)
) : (
<>
{PageTitle}
{CardContent}
</>
)}
</Col>
</Row>
</Layout>
);
};
export default AuthPage;
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
// eslint-disable-next-line
const root = createRoot(container!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+40
View File
@@ -0,0 +1,40 @@
import { useGetIdentity, usePermissions } from "@refinedev/core";
import { Row, Col, Card, Avatar, Typography, Space } from "antd";
const { Text } = Typography;
export const DashboardPage: React.FC = () => {
const { data: identity } = useGetIdentity<{
id: string;
name: string;
avatar: string;
}>();
const permissions = usePermissions<string[], { permissions: string[] }>({
params: { permissions: ["admin"] },
});
return (
<Row gutter={20}>
<Col span={6}>
<Card
title="Identity"
style={{ height: "300px", borderRadius: "15px" }}
headStyle={{ textAlign: "center" }}
>
<Space align="center" direction="horizontal">
<Avatar size="large" src={identity?.avatar} />
<Text>{identity?.name}</Text>
</Space>
</Card>
</Col>
<Col span={6}>
<Card
title="Permissions"
style={{ height: "300px", borderRadius: "15px" }}
headStyle={{ textAlign: "center" }}
>
<Text>{permissions.data}</Text>
</Card>
</Col>
</Row>
);
};
@@ -0,0 +1,33 @@
import { Create, useForm } from "@refinedev/antd";
import { Form, Input } from "antd";
import MDEditor from "@uiw/react-md-editor";
import type { Note } from "../../shapes/orm/post.typings";
export const PostCreate = () => {
const { formProps, saveButtonProps } = useForm<Note>();
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input placeholder="Enter a post title" />
</Form.Item>
<Form.Item
label="Content"
name="content"
>
<MDEditor data-color-mode="light" />
</Form.Item>
</Form>
</Create>
);
};
@@ -0,0 +1,68 @@
import React, { useCallback, useState } from "react";
import { Edit, useForm, RefreshButton } from "@refinedev/antd";
import { useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
import { Alert, Form, Input } from "antd";
import MDEditor from "@uiw/react-md-editor";
import type { Note } from "../../shapes/orm/post.typings";
export const PostEdit = () => {
const [deprecated, setDeprecated] = useState<boolean>(false);
const { list } = useNavigation();
const { open } = useNotification();
const onLiveEvent = useCallback((event: LiveEvent) => {
console.log('onLiveEvent', event);
if (event.type === "deleted") {
open?.({
type: "error",
message: "This post has been deleted",
});
list("posts");
} else if (event.type === "updated") {
setDeprecated(true);
}
}, [open, list]);
const { formProps, saveButtonProps, query: queryResult } = useForm<Note>({
liveMode: 'manual',
onLiveEvent,
});
const handleRefresh = () => {
queryResult?.refetch();
setDeprecated(false);
};
return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
{deprecated && (
<Alert
message="This post has been updated. Refresh to see changes."
type="warning"
style={{ marginBottom: 20 }}
action={<RefreshButton size="small" onClick={handleRefresh} />}
/>
)}
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Content"
name="content"
>
<MDEditor data-color-mode="light" />
</Form.Item>
</Form>
</Edit>
);
};
@@ -0,0 +1,4 @@
export * from "./list";
export * from "./create";
export * from "./edit";
export * from "./show";
@@ -0,0 +1,49 @@
import {
List,
useTable,
EditButton,
ShowButton,
DeleteButton,
CreateButton,
} from "@refinedev/antd";
import { useNavigation } from "@refinedev/core";
import { Table, Space } from "antd";
import type { Note } from "../../shapes/orm/post.typings";
const shortenNuri = (nuri: string) => {
return nuri?.split(':')[3]?.slice(0, 7);
};
export const PostList = () => {
const { tableProps } = useTable<Note>();
const { show } = useNavigation();
return (
<List headerButtons={<CreateButton />}>
<Table
{...tableProps}
rowKey="@graph"
onRow={(record) => ({
onClick: () => show("posts", record["@graph"]),
style: { cursor: "pointer" },
})}
>
<Table.Column dataIndex="@graph" title="ID" render={shortenNuri} />
<Table.Column dataIndex="title" title="Title" />
<Table.Column dataIndex="content" title="Content" />
<Table.Column<Note>
title="Actions"
dataIndex="actions"
align="right"
render={(_, record) => (
<Space onClick={(e) => e.stopPropagation()}>
<EditButton hideText size="small" recordItemId={record['@graph']} />
<ShowButton hideText size="small" recordItemId={record['@graph']} />
<DeleteButton hideText size="small" recordItemId={record['@graph']} />
</Space>
)}
/>
</Table>
</List>
);
};
@@ -0,0 +1,37 @@
import { useCallback } from "react";
import { useShow, useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
import { Show, MarkdownField } from "@refinedev/antd";
import { Typography } from "antd";
import type { Note } from "../../shapes/orm/post.typings";
const { Title, Text } = Typography;
export const PostShow = () => {
const { list } = useNavigation();
const { open } = useNotification();
const onLiveEvent = useCallback((event: LiveEvent) => {
if (event.type === "deleted") {
open?.({
type: "error",
message: "This post has been deleted",
});
list("posts");
}
}, [open, list]);
const { query: queryResult } = useShow<Note>({ liveMode: "auto", onLiveEvent });
const { data, isLoading } = queryResult;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Title level={5}>Id</Title>
<Text>{record?.['@graph']}</Text>
<Title level={5}>Title</Title>
<Text>{record?.title}</Text>
<Title level={5}>Content</Title>
<MarkdownField value={record?.content} />
</Show>
);
};
@@ -0,0 +1,10 @@
import { DataModels } from "@ng-org/refine-providers";
import { NoteShapeType } from "./orm/post.shapeTypes";
const dataModels : DataModels = {
posts: {
shapeType: NoteShapeType
}
}
export default dataModels;
@@ -0,0 +1,48 @@
import type { Schema } from "@ng-org/shex-orm";
/**
* =============================================================================
* postSchema: Schema for post
* =============================================================================
*/
export const postSchema = {
"https://www.w3.org/ns/activitystreams#Note": {
iri: "https://www.w3.org/ns/activitystreams#Note",
predicates: [
{
dataTypes: [
{
valType: "iri",
literals: ["https://www.w3.org/ns/activitystreams#Note"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "https://www.w3.org/ns/activitystreams#title",
readablePredicate: "title",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "https://www.w3.org/ns/activitystreams#content",
readablePredicate: "content",
},
],
},
} as const satisfies Schema;
@@ -0,0 +1,9 @@
import type { ShapeType } from "@ng-org/shex-orm";
import { postSchema } from "./post.schema";
import type { Note } from "./post.typings";
// ShapeTypes for post
export const NoteShapeType = {
schema: postSchema,
shape: "https://www.w3.org/ns/activitystreams#Note",
} as const satisfies ShapeType<Note>;
@@ -0,0 +1,37 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for post
* =============================================================================
*/
/**
* Note Type
*/
export interface Note {
/**
* The graph NURI.
*/
readonly "@graph": IRI;
/**
* The subject IRI.
*/
readonly "@id": IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
"@type": "https://www.w3.org/ns/activitystreams#Note";
/**
* The title of the post
*
* Original IRI: https://www.w3.org/ns/activitystreams#title
*/
title: string;
/**
* The content of the post
*
* Original IRI: https://www.w3.org/ns/activitystreams#content
*/
content?: string;
}
@@ -0,0 +1,11 @@
PREFIX as: <https://www.w3.org/ns/activitystreams#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
as:Note {
a [as:Note] ;
as:title xsd:string
// rdfs:comment "The title of the post" ;
as:content xsd:string ?
// rdfs:comment "The content of the post" ;
}
+17
View File
@@ -0,0 +1,17 @@
import { ng, init, Session } from "@ng-org/web";
import { initNg } from "@ng-org/orm";
export let session: Session | undefined;
init(
async (event: any) => {
session = event.session;
// The ORM needs to have access to ng,
// the interface to the engine running in WASM.
initNg(ng, session!);
},
true,
[],
);
export const getSession = () => session;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />