diff --git a/services/web/app/views/project/ide-react.pug b/services/web/app/views/project/ide-react.pug
index 3d7b5213e6..283629f294 100644
--- a/services/web/app/views/project/ide-react.pug
+++ b/services/web/app/views/project/ide-react.pug
@@ -9,6 +9,10 @@ block vars
block entrypointVar
- entrypoint = 'pages/ide'
+block css
+ each file in entrypointStyles('ide')
+ link(rel='stylesheet', href=file)
+
block content
main#ide-root
.loading-screen
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index c7c9e72960..6fa147eb0a 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -260,6 +260,7 @@
"disable_sso": "",
"disable_stop_on_first_error": "",
"disabling": "",
+ "disconnected": "",
"discount_of": "",
"dismiss": "",
"dismiss_error_popup": "",
@@ -307,6 +308,7 @@
"editing": "",
"editing_captions": "",
"editor_and_pdf": "&",
+ "editor_disconected_click_to_reconnect": "",
"editor_only_hide_pdf": "",
"editor_theme": "",
"educational_discount_for_groups_of_x_or_more": "",
@@ -631,6 +633,7 @@
"logs_and_output_files": "",
"looking_multiple_licenses": "",
"looks_like_youre_at": "",
+ "lost_connection": "",
"main_document": "",
"main_file_not_found": "",
"make_a_copy": "",
@@ -678,6 +681,7 @@
"month": "",
"more": "",
"more_actions": "",
+ "more_info": "",
"more_options_for_border_settings_coming_soon": "",
"my_library": "",
"n_items": "",
@@ -887,6 +891,8 @@
"recompile_from_scratch": "",
"recompile_pdf": "",
"reconnect": "",
+ "reconnecting": "",
+ "reconnecting_in_x_secs": "",
"recurly_email_update_needed": "",
"recurly_email_updated": "",
"redirect_to_editor": "",
@@ -1102,6 +1108,7 @@
"sync_project_to_github_explanation": "",
"sync_to_dropbox": "",
"sync_to_github": "",
+ "synctex_failed": "",
"syntax_validation": "",
"tab_connecting": "",
"tab_no_longer_connected": "",
@@ -1222,6 +1229,7 @@
"tried_to_register_with_email": "",
"try_again": "",
"try_it_for_free": "",
+ "try_now": "",
"try_premium_for_free": "",
"try_recompile_project_or_troubleshoot": "",
"try_relinking_provider": "",
diff --git a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
new file mode 100644
index 0000000000..f76544df50
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
@@ -0,0 +1,73 @@
+import { useTranslation } from 'react-i18next'
+import { LostConnectionAlert } from './lost-connection-alert'
+import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import { debugging } from '@/utils/debugging'
+import { Alert } from 'react-bootstrap'
+
+// TODO SavingNotificationController, SystemMessagesController, out-of-sync modal
+export function Alerts() {
+ const { t } = useTranslation()
+ const {
+ connectionState,
+ isConnected,
+ isStillReconnecting,
+ tryReconnectNow,
+ secondsUntilReconnect,
+ } = useConnectionContext()
+
+ // TODO: Get this from a context
+ const synctexError = false
+
+ return (
+
+ {connectionState.forceDisconnected ? (
+
+ {t('disconnected')}
+
+ ) : null}
+
+ {connectionState.reconnectAt ? (
+
+ ) : null}
+
+ {isStillReconnecting ? (
+
+ {t('reconnecting')}…
+
+ ) : null}
+
+ {synctexError ? (
+
+ {t('synctex_failed')}
+
+ {t('more_info')}
+
+
+ ) : null}
+
+ {connectionState.inactiveDisconnect ||
+ (connectionState.readyState === WebSocket.CLOSED &&
+ (connectionState.error === 'rate-limited' ||
+ connectionState.error === 'unable-to-connect') &&
+ !secondsUntilReconnect()) ? (
+
+ {t('editor_disconected_click_to_reconnect')}
+
+ ) : null}
+
+ {debugging ? (
+
+ Connected: {isConnected.toString()}
+
+ ) : null}
+
+ )
+}
diff --git a/services/web/frontend/js/features/ide-react/components/alerts/lost-connection-alert.tsx b/services/web/frontend/js/features/ide-react/components/alerts/lost-connection-alert.tsx
new file mode 100644
index 0000000000..6592491bab
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/components/alerts/lost-connection-alert.tsx
@@ -0,0 +1,40 @@
+import { useTranslation } from 'react-i18next'
+import { useEffect, useState } from 'react'
+import { secondsUntil } from '@/features/ide-react/connection/utils'
+import { Alert } from 'react-bootstrap'
+
+type LostConnectionAlertProps = {
+ reconnectAt: number
+ tryReconnectNow: () => void
+}
+
+export function LostConnectionAlert({
+ reconnectAt,
+ tryReconnectNow,
+}: LostConnectionAlertProps) {
+ const { t } = useTranslation()
+ const [secondsUntilReconnect, setSecondsUntilReconnect] = useState(
+ secondsUntil(reconnectAt)
+ )
+
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ setSecondsUntilReconnect(secondsUntil(reconnectAt))
+ }, 1000)
+ return () => window.clearInterval(timer)
+ }, [reconnectAt])
+
+ return (
+
+ {t('lost_connection')}{' '}
+ {t('reconnecting_in_x_secs', { seconds: secondsUntilReconnect })}.
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
index 795def99d6..90c5185acb 100644
--- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
+++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
@@ -2,6 +2,7 @@ import LayoutWithPlaceholders from '@/features/ide-react/components/layout/layou
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import useEventListener from '@/shared/hooks/use-event-listener'
import { useCallback, useEffect } from 'react'
+import { Alerts } from '@/features/ide-react/components/alerts/alerts'
// This is filled with placeholder content while the real content is migrated
// away from Angular
@@ -23,7 +24,8 @@ export default function IdePage() {
return (
<>
- {/* TODO: Alerts and left menu will go here */}
+
+ {/* TODO: Left menu will go here */}
>
)
diff --git a/services/web/frontend/stylesheets/app/editor/ide-react.less b/services/web/frontend/stylesheets/app/editor/ide-react.less
index 51d611fa01..9ad548f92b 100644
--- a/services/web/frontend/stylesheets/app/editor/ide-react.less
+++ b/services/web/frontend/stylesheets/app/editor/ide-react.less
@@ -1,5 +1,12 @@
#ide-root {
height: 100vh;
+
+ .global-alerts {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
}
.ide-react-main {
diff --git a/services/web/frontend/stylesheets/core/mixins.less b/services/web/frontend/stylesheets/core/mixins.less
index 998d394a0a..ca219ddd76 100755
--- a/services/web/frontend/stylesheets/core/mixins.less
+++ b/services/web/frontend/stylesheets/core/mixins.less
@@ -543,6 +543,7 @@
color: darken(@text-color, 10%);
}
+ button,
.alert-link-as-btn {
display: inline-block;
font-weight: bold;
@@ -557,6 +558,10 @@
}
}
+ button {
+ border-width: 0;
+ }
+
small,
.small {
color: @text-color;