Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_API_HOST = http://localhost:8080
72 changes: 65 additions & 7 deletions components/Project/EvaluationDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import { RequirementEvaluation, UserRole } from '@idea2app/data-server';
import {
PrototypeType,
PrototypeVersion,
RequirementEvaluation,
UserRole,
} from '@idea2app/data-server';
import { Box, Typography } from '@mui/material';
import { observer } from 'mobx-react';
import { FC, useContext } from 'react';

import { I18nContext } from '../../models/Translation';
import { i18n, I18nContext } from '../../models/Translation';
import userStore from '../../models/User';
import { PrototypeGenerator, PrototypeGeneratorProps } from './PrototypeGenerator';

export const EvaluationDisplay: FC<RequirementEvaluation> = observer(
export const DevelopmentScopeName = ({ t }: typeof i18n) => [
t('product_prototype'),
t('ui_design'),
t('desktop'),
t('mobile'),
t('server'),
];

export interface EvaluationDisplayProps
extends RequirementEvaluation,
Pick<PrototypeGeneratorProps, 'projectId' | 'messageId'> {
prototypes?: PrototypeVersion[];
}

export const EvaluationDisplay: FC<EvaluationDisplayProps> = observer(
({
title,
scopes = [],
models,
developerCount,
designerCount,
workload,
monthPeriod,
budget,
factor,
projectId,
messageId,
prototypes,
}) => {
const { t } = useContext(I18nContext),
const i18n = useContext(I18nContext);
const { t } = i18n,
{ roles } = userStore.session || {};

return (
<Box
className="prose"
sx={{
'& .evaluation-item': {
marginBottom: 1,
Expand Down Expand Up @@ -53,9 +79,41 @@ export const EvaluationDisplay: FC<RequirementEvaluation> = observer(
{t('development_scopes')}
</Typography>
<Box component="ul" sx={{ mt: 0.5 }}>
{scopes.map(scope => (
<Box key={scope} component="li" sx={{ ml: 1 }}>
{scope}
{scopes.map(scope => {
const prototypeType = (
scope === 2 ? 'desktop' : scope === 3 ? 'mobile' : undefined
) as PrototypeType;

return (
<Box
key={scope}
component="li"
sx={{ ml: 1, display: 'flex', alignItems: 'center', gap: 1 }}
>
{DevelopmentScopeName(i18n)[scope]}

{prototypeType && (
<PrototypeGenerator
{...{ projectId, messageId }}
type={prototypeType}
prototype={prototypes?.find(({ type }) => type === prototypeType)}
/>
)}
</Box>
);
})}
</Box>
</Box>
)}
{models?.[0] && (
<Box className="evaluation-item">
<Typography component="h4" sx={{ fontWeight: 600 }}>
{t('feature_modules')}
</Typography>
<Box component="ol" sx={{ mt: 0.5 }}>
{models.map((model, index) => (
<Box key={index} component="li" sx={{ ml: 1 }}>
{model}
</Box>
))}
</Box>
Expand Down
50 changes: 32 additions & 18 deletions components/Project/NewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,36 @@ import Link from 'next/link';
import { FC, useContext } from 'react';

import { I18nContext } from '../../models/Translation';
import type zhCN from '../../translation/zh-CN';

export const ProjectCard: FC<Project> = observer(({ id, name, projectStatus }) => {
const statusTextKeys: (keyof typeof zhCN)[] = [
'project_open', // Open
'project_evaluated', // Evaluated
'project_contract_generated', // ContractGenerated
'project_in_development', // InDevelopment
'project_in_testing', // InTesting
'project_maintenance', // Maintenance
];

const bgColors: string[] = [
'grey.200', // Open
'success.light', // Evaluated
'warning.light', // ContractGenerated
'info.light', // InDevelopment
'secondary.light', // InTesting
'primary.light', // Maintenance
];

const textColors: string[] = [
'text.primary', // Open
'success.contrastText', // Evaluated
'warning.contrastText', // ContractGenerated
'info.contrastText', // InDevelopment
'secondary.contrastText', // InTesting
'primary.contrastText', // Maintenance
];

export const ProjectCard: FC<Project> = observer(({ id, name, status = 0 }) => {
const { t } = useContext(I18nContext);

return (
Expand All @@ -21,25 +49,11 @@ export const ProjectCard: FC<Project> = observer(({ id, name, projectStatus }) =
px: 1,
py: 0.5,
borderRadius: 1,
bgcolor:
projectStatus === '1'
? 'success.light'
: projectStatus === '0'
? 'grey.200'
: 'warning.light',
color:
projectStatus === '1'
? 'success.contrastText'
: projectStatus === '0'
? 'text.primary'
: 'warning.contrastText',
bgcolor: bgColors[status] ?? 'grey.200',
color: textColors[status] ?? 'text.primary',
}}
>
{projectStatus === '1'
? t('project_open')
: projectStatus === '0'
? t('project_closed')
: t('project_pending')}
{t((statusTextKeys[status] ?? 'project_open') as keyof typeof zhCN)}
</Typography>
</CardContent>
<CardActions>
Expand Down
170 changes: 170 additions & 0 deletions components/Project/PrototypeGenerator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { PrototypeType, PrototypeVersion } from '@idea2app/data-server';
import { Box, Button, CircularProgress, Link, Typography } from '@mui/material';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
import { createRef } from 'react';
import { inViewport, sleep } from 'web-utility';

import { PrototypeVersionModel } from '../../models/PrototypeVersion';
import { i18n, I18nContext } from '../../models/Translation';

export interface PrototypeGeneratorProps {
projectId: number;
messageId: number;
type: PrototypeType;
prototype?: PrototypeVersion;
}

@observer
export class PrototypeGenerator extends ObservedComponent<PrototypeGeneratorProps, typeof i18n> {
static contextType = I18nContext;

versionStore = new PrototypeVersionModel(this.props.projectId, this.props.type);

@observable
accessor version = this.props.prototype;

private root = createRef<HTMLElement>();

componentDidMount() {
super.componentDidMount();

this.pollStatusCheck();
}

async pollStatusCheck() {
const { props, version } = this,
rootElement = this.root.current;

while (version?.status === 'pending' || version?.status === 'processing') {
if (!rootElement?.isConnected) break;

if (inViewport(rootElement))
this.version = await this.versionStore.getOne(props.prototype!.id);

await sleep(3);
}
}

handleGenerateClick = async () => {
this.version = await this.versionStore.updateOne({
evaluationMessage: this.props.messageId,
});

return this.pollStatusCheck();
};

renderPending() {
const { t } = this.observedContext;
const loading = this.versionStore.uploading > 0;

return (
<Button
variant="contained"
color="primary"
size="small"
disabled={loading}
sx={{ textTransform: 'none' }}
onClick={this.handleGenerateClick}
>
{loading ? t('generating') : t('generate_prototype')}
</Button>
);
}

renderGenerating() {
const { t } = this.observedContext;

return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="body2">{t('prototype_generating')}</Typography>
</Box>
);
}

renderCompleted() {
const { t } = this.observedContext;
const { previewLink, gitLogsLink } = this.version || {};

return (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{previewLink && (
<Link
href={previewLink}
target="_blank"
rel="noopener noreferrer"
sx={{
textDecoration: 'none',
fontSize: '0.875rem',
fontWeight: 500,
color: 'primary.main',
}}
>
{t('view_preview')}
</Link>
)}
{gitLogsLink && (
<Link
href={gitLogsLink}
target="_blank"
rel="noopener noreferrer"
sx={{
textDecoration: 'none',
fontSize: '0.875rem',
fontWeight: 500,
color: 'text.secondary',
}}
>
{t('view_ai_log')}
</Link>
)}
</Box>
);
}

renderFailed() {
const { t } = this.observedContext;
const { errorMessage, gitLogsLink } = this.version || {};

return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="body2" color="error" sx={{ fontSize: '0.875rem' }}>
{errorMessage || t('prototype_generation_failed')}
</Typography>
{gitLogsLink && (
<Link
href={gitLogsLink}
target="_blank"
rel="noopener noreferrer"
sx={{
textDecoration: 'none',
fontSize: '0.875rem',
fontWeight: 500,
color: 'text.secondary',
}}
>
{t('view_ai_log')}
</Link>
)}
</Box>
);
}

render() {
const { version } = this;

return (
<Box ref={this.root} sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
{!version || version.status === 'pending'
? this.renderPending()
: version.status === 'processing'
? this.renderGenerating()
: version.status === 'completed'
? this.renderCompleted()
: this.renderFailed()}
</Box>
);
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion components/Project/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from 'react';

import { Project } from '../../models/Project';
import { ProjectCard } from './Card';
import { ProjectCard } from './PublicCard';

export interface ProjectListLayoutProps {
defaultData: Project[];
Expand Down
32 changes: 20 additions & 12 deletions components/ScrollBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ export type ScrollBoundaryProps = PropsWithChildren<
}
>;

function touch(edge: EdgePosition, onTouch: TouchHandler) {
return (node: HTMLElement | null) => {
if (node) {
new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
onTouch(edge);
}
}).observe(node);
}
};
}
const EdgeOrder: EdgePosition[] = ['top', 'right', 'bottom', 'left'];

const touch = (edge: EdgePosition, onTouch: TouchHandler) => (node: HTMLElement | null) => {
if (!node) return;

const anchor = node.parentElement?.parentElement;

const { overflowX, overflowY } = anchor ? getComputedStyle(anchor) : {};

const root = `${overflowX}${overflowY}`.match(/auto|scroll/) ? anchor : null;

const edgeMargins = Array(4).fill('0px');
edgeMargins[EdgeOrder.indexOf(edge)] = '200px';

new IntersectionObserver(([{ isIntersecting }]) => isIntersecting && onTouch(edge), {
root,
rootMargin: edgeMargins.join(' '),
}).observe(node);
};

export const ScrollBoundary: FC<ScrollBoundaryProps> = ({
className = '',
Expand All @@ -30,7 +38,7 @@ export const ScrollBoundary: FC<ScrollBoundaryProps> = ({
left,
right,
bottom,
children
children,
}) => (
<div className={className} style={{ position: 'relative' }}>
<div
Expand Down
Loading
Loading