diff --git a/backend/src/common/models.ts b/backend/src/common/models.ts index 7f9e26dac..80e89f2a7 100644 --- a/backend/src/common/models.ts +++ b/backend/src/common/models.ts @@ -35,4 +35,5 @@ export const defaultSession: Omit = { locked: false, ready: [], timer: null, + demo: false, }; diff --git a/backend/src/common/types.ts b/backend/src/common/types.ts index 198f07819..57d77980a 100644 --- a/backend/src/common/types.ts +++ b/backend/src/common/types.ts @@ -10,6 +10,7 @@ export interface Session extends PostContainer, Entity { createdBy: User; ready: string[]; timer: Date | null; + demo: boolean; } export interface SessionMetadata extends Entity { @@ -218,6 +219,7 @@ export type TrackingEvent = | 'register/password' | 'register/oauth' | 'register/anonymous' + | 'register/demo' | 'subscribe/initial' | 'subscribe/launch-stripe' | 'subscribe/purchased' @@ -255,5 +257,7 @@ export type StripeLocales = | 'ja-JP' | 'nl-NL' | 'pt-BR' + | 'pt-PT' | 'no-NO' + | 'uk-UA' | 'sv-S'; diff --git a/backend/src/db/actions/demo.ts b/backend/src/db/actions/demo.ts new file mode 100644 index 000000000..5a55b2245 --- /dev/null +++ b/backend/src/db/actions/demo.ts @@ -0,0 +1,84 @@ +import { v4 } from 'uuid'; +import { Post, Session } from '../../common/types.js'; +import { UserEntity } from '../../db/entities/UserIdentity.js'; +import { savePost, saveVote } from './posts.js'; +import { createSession, getSession, saveSession } from './sessions.js'; +import { getNext, getMiddle } from '../../lexorank.js'; +import { registerAnonymousUser } from './users.js'; +import { DeepPartial } from 'typeorm'; + +export async function createDemoSession(author: UserEntity): Promise { + const session = await createSession(author); + session.name = 'My Retro Demo'; + session.demo = true; + session.options = { + ...session.options, + allowActions: true, + allowGiphy: true, + allowMultipleVotes: true, + allowReordering: true, + allowSelfVoting: true, + allowTimer: true, + allowCancelVote: true, + allowGrouping: true, + maxDownVotes: 20, + maxUpVotes: 20, + readonlyOnTimerEnd: true, + }; + await saveSession(author.id, session); + let rank = getMiddle(); + + const otherUser = (await registerAnonymousUser('John Doe^' + v4(), v4()))!; + + const otherUserId = otherUser!.user!.id; + + async function createPost( + content: string, + column: number, + votes = 0, + own = false + ) { + rank = getNext(rank); + const postData: DeepPartial = { + content, + column, + giphy: null, + action: null, + rank, + votes: [], + group: null, + id: v4(), + }; + const post = (await savePost( + own ? author.id : otherUserId, + session.id, + postData + ))!; + for (let i = 0; i < votes; i++) { + saveVote(otherUserId, '', post.id, { + id: v4(), + type: 'like', + user: otherUser.user.toJson(), + }); + } + const updatedSession = await getSession(session.id); + return updatedSession; + } + + await Promise.all([ + createPost("I'm enjoying our new retrospective board!", 0, 5), + createPost('I love how we can vote on posts', 0), + createPost('I wish I discovered this tool sooner 😅', 1, 2, true), + createPost( + 'Have you tried different settings? Click on "Customise" to see what you can do.', + 2 + ), + createPost('Try Giphy by clicking on the yellow smiley face!', 2, 1, true), + createPost( + 'You can also share this URL with somebody else to collaborate on the same board.', + 2 + ), + ]); + + return session; +} diff --git a/backend/src/db/actions/posts.ts b/backend/src/db/actions/posts.ts index 790d887ad..752c134e1 100644 --- a/backend/src/db/actions/posts.ts +++ b/backend/src/db/actions/posts.ts @@ -1,3 +1,4 @@ +import { DeepPartial } from 'typeorm'; import { Post, PostGroup, Vote } from '../../common/index.js'; import { PostRepository, @@ -17,7 +18,7 @@ export async function getNumberOfPosts(userId: string): Promise { export async function savePost( userId: string, sessionId: string, - post: Post + post: DeepPartial ): Promise { return await transaction(async (manager) => { const postRepository = manager.withRepository(PostRepository); diff --git a/backend/src/db/entities/Session.ts b/backend/src/db/entities/Session.ts index 5fa22eaa1..744cdf418 100644 --- a/backend/src/db/entities/Session.ts +++ b/backend/src/db/entities/Session.ts @@ -64,6 +64,8 @@ export default class SessionEntity { visitors: UserEntity[] | undefined; @Column({ default: false }) public locked: boolean; + @Column({ default: false }) + public demo: boolean; @Column({ default: null, type: 'timestamp with time zone', nullable: true }) public timer: Date | null; @Column('text', { array: true, default: '{}' }) @@ -90,6 +92,7 @@ export default class SessionEntity { locked: this.locked, ready: this.ready, timer: this.timer, + demo: this.demo, }; } @@ -107,5 +110,6 @@ export default class SessionEntity { this.locked = false; this.ready = []; this.timer = null; + this.demo = false; } } diff --git a/backend/src/db/migrations/1677668065305-Demo.ts b/backend/src/db/migrations/1677668065305-Demo.ts new file mode 100644 index 000000000..49a5576ff --- /dev/null +++ b/backend/src/db/migrations/1677668065305-Demo.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Demo1677668065305 implements MigrationInterface { + name = 'Demo1677668065305' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" ADD "demo" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "demo"`); + } + +} diff --git a/backend/src/db/repositories/PostRepository.ts b/backend/src/db/repositories/PostRepository.ts index 92e88759e..1e1c6c546 100644 --- a/backend/src/db/repositories/PostRepository.ts +++ b/backend/src/db/repositories/PostRepository.ts @@ -3,6 +3,7 @@ import SessionRepository from './SessionRepository.js'; import { Post as JsonPost, defaultSession } from '../../common/index.js'; import { cloneDeep } from 'lodash-es'; import { getBaseRepository, saveAndReload } from './BaseRepository.js'; +import { DeepPartial } from 'typeorm'; export default getBaseRepository(PostEntity).extend({ async updateFromJson( @@ -28,7 +29,7 @@ export default getBaseRepository(PostEntity).extend({ async saveFromJson( sessionId: string, userId: string, - post: JsonPost + post: DeepPartial ): Promise { const session = await this.manager.findOne(SessionEntity, { where: { id: sessionId }, diff --git a/backend/src/index.ts b/backend/src/index.ts index 69775771b..648719853 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -66,6 +66,7 @@ import mung from 'express-mung'; import { QueryFailedError } from 'typeorm'; import { deleteAccount } from './db/actions/delete.js'; import { noop } from 'lodash-es'; +import { createDemoSession } from './db/actions/demo.js'; const realIpHeader = 'X-Forwarded-For'; const sessionSecret = `${config.SESSION_SECRET!}-4.11.5`; // Increment to force re-auth @@ -281,6 +282,29 @@ db().then(() => { }); }); + // Create a demo session + app.post('/api/demo', heavyLoadLimiter, async (req, res) => { + const identity = await getIdentityFromRequest(req); + setScope(async (scope) => { + if (identity) { + try { + const session = await createDemoSession(identity.user); + res.status(200).send(session); + } catch (err: unknown) { + if (err instanceof QueryFailedError) { + reportQueryError(scope, err); + } + res.status(500).send(); + throw err; + } + } else { + res + .status(401) + .send('You must be logged in in order to create a session'); + } + }); + }); + app.post('/api/logout', async (req, res, next) => { req.logout({ keepSessionInfo: false }, noop); req.session?.destroy((err: string) => { diff --git a/backend/src/lexorank.ts b/backend/src/lexorank.ts new file mode 100644 index 000000000..a5bf19947 --- /dev/null +++ b/backend/src/lexorank.ts @@ -0,0 +1,21 @@ +import { LexoRank } from 'lexorank'; + +export function getMiddle(): string { + return LexoRank.middle().toString(); +} + +export function getNext(rank: string): string { + return LexoRank.parse(rank).genNext().toString(); +} + +export function getPrevious(rank: string): string { + return LexoRank.parse(rank).genPrev().toString(); +} + +export function getBetween(before: string, after: string): string { + try { + return LexoRank.parse(before).between(LexoRank.parse(after)).toString(); + } catch { + return before; + } +} diff --git a/frontend/crowdin.yml b/frontend/crowdin.yml index 27710513a..b4d6e1922 100644 --- a/frontend/crowdin.yml +++ b/frontend/crowdin.yml @@ -1,11 +1,11 @@ "project_id" : "512896" -"base_path" : "." +"base_path" : "../" "base_url" : "https://api.crowdin.com" "preserve_hierarchy": true files: [ { - "source" : "/src/translations/locales/en-GB.json", - "translation" : "/src/translations/locales/%locale%.json" + "source" : "frontend/src/translations/locales/en-GB.json", + "translation" : "frontend/src/translations/locales/%locale%.json" } ] \ No newline at end of file diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 5200c0519..fe1d5623e 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -44,6 +44,7 @@ const Invite = lazy(() => import('./views/layout/Invite')); const Panel = lazy(() => import('./views/Panel')); const EncryptionDoc = lazy(() => import('./views/home/Encryption')); const AdminPage = lazy(() => import('./views/admin/AdminPage')); +const Demo = lazy(() => import('./views/Demo')); const Title = styled(Typography)` color: white; @@ -141,6 +142,7 @@ function App() { } /> } /> } /> + } /> }> } /> } /> diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a4fc818aa..a213028a7 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -27,6 +27,10 @@ export async function createGame(): Promise { return await fetchPostGet('/api/create', null); } +export async function createDemoGame(): Promise { + return await fetchPostGet('/api/demo', null); +} + export async function createEncryptedGame( encryptionKey: string ): Promise { diff --git a/frontend/src/common/models.ts b/frontend/src/common/models.ts index 7c93cb3e5..80e89f2a7 100644 --- a/frontend/src/common/models.ts +++ b/frontend/src/common/models.ts @@ -1,4 +1,4 @@ -import { SessionOptions, Session } from './types'; +import { SessionOptions, Session } from './types.js'; export const defaultOptions: SessionOptions = { allowActions: true, @@ -11,9 +11,9 @@ export const defaultOptions: SessionOptions = { allowGiphy: true, allowGrouping: true, allowReordering: true, + allowCancelVote: true, blurCards: false, newPostsFirst: true, - allowCancelVote: true, allowTimer: true, timerDuration: 15 * 60, readonlyOnTimerEnd: true, @@ -35,4 +35,5 @@ export const defaultSession: Omit = { locked: false, ready: [], timer: null, + demo: false, }; diff --git a/frontend/src/common/types.ts b/frontend/src/common/types.ts index 198f07819..57d77980a 100644 --- a/frontend/src/common/types.ts +++ b/frontend/src/common/types.ts @@ -10,6 +10,7 @@ export interface Session extends PostContainer, Entity { createdBy: User; ready: string[]; timer: Date | null; + demo: boolean; } export interface SessionMetadata extends Entity { @@ -218,6 +219,7 @@ export type TrackingEvent = | 'register/password' | 'register/oauth' | 'register/anonymous' + | 'register/demo' | 'subscribe/initial' | 'subscribe/launch-stripe' | 'subscribe/purchased' @@ -255,5 +257,7 @@ export type StripeLocales = | 'ja-JP' | 'nl-NL' | 'pt-BR' + | 'pt-PT' | 'no-NO' + | 'uk-UA' | 'sv-S'; diff --git a/frontend/src/testing/index.tsx b/frontend/src/testing/index.tsx index 86cbf6696..398aef01f 100644 --- a/frontend/src/testing/index.tsx +++ b/frontend/src/testing/index.tsx @@ -30,6 +30,7 @@ export const initialSession: Session = { }, ready: [], timer: null, + demo: false, }; export function AllTheProviders({ children }: PropsWithChildren<{}>) { diff --git a/frontend/src/translations/languages.ts b/frontend/src/translations/languages.ts index 616d58311..51df09644 100644 --- a/frontend/src/translations/languages.ts +++ b/frontend/src/translations/languages.ts @@ -23,9 +23,10 @@ export interface Language { dateLocale: () => Promise<{ default: Locale }>; stripeLocale: StripeLocales; locale: string; + twoLetter: string; } -export default [ +const languages: Language[] = [ { dateLocale: enGB, iso: 'gb', @@ -33,6 +34,7 @@ export default [ englishName: 'English', stripeLocale: 'en-US', locale: 'en-GB', + twoLetter: 'en', }, { dateLocale: fr, @@ -41,6 +43,7 @@ export default [ englishName: 'French', stripeLocale: 'fr-FR', locale: 'fr-FR', + twoLetter: 'fr', }, { dateLocale: de, @@ -49,6 +52,7 @@ export default [ englishName: 'German', stripeLocale: 'de-DE', locale: 'de-DE', + twoLetter: 'de', }, { dateLocale: es, @@ -57,6 +61,7 @@ export default [ englishName: 'Spanish', stripeLocale: 'es-ES', locale: 'es-ES', + twoLetter: 'es', }, { dateLocale: arDZ, @@ -65,6 +70,7 @@ export default [ englishName: 'Arabic', stripeLocale: 'ar-AR', locale: 'ar-SA', + twoLetter: 'ar', }, { dateLocale: zhCN, @@ -73,6 +79,7 @@ export default [ englishName: 'Chinese (Simplified)', stripeLocale: 'en-US', locale: 'zh-CN', + twoLetter: 'zh', }, { dateLocale: zhTW, @@ -81,6 +88,7 @@ export default [ englishName: 'Chinese (Traditional)', stripeLocale: 'en-US', locale: 'zh-TW', + twoLetter: 'zh', }, { dateLocale: nl, @@ -89,6 +97,7 @@ export default [ englishName: 'Dutch', stripeLocale: 'nl-NL', locale: 'nl-NL', + twoLetter: 'nl', }, { dateLocale: hu, @@ -97,6 +106,7 @@ export default [ englishName: 'Hungarian', stripeLocale: 'en-US', locale: 'hu-HU', + twoLetter: 'hu', }, { dateLocale: it, @@ -105,6 +115,7 @@ export default [ englishName: 'Italian', stripeLocale: 'it-IT', locale: 'it-IT', + twoLetter: 'it', }, { dateLocale: ja, @@ -113,6 +124,7 @@ export default [ englishName: 'Japanese', stripeLocale: 'ja-JP', locale: 'ja-JP', + twoLetter: 'ja', }, { dateLocale: pl, @@ -121,6 +133,7 @@ export default [ englishName: 'Polish', stripeLocale: 'en-US', locale: 'pl-PL', + twoLetter: 'pl', }, { dateLocale: ptBR, @@ -129,6 +142,7 @@ export default [ englishName: 'Portuguese (Brazilian)', stripeLocale: 'pt-BR', locale: 'pt-BR', + twoLetter: 'pt', }, { dateLocale: pt, @@ -137,6 +151,7 @@ export default [ englishName: 'Portuguese (Portugal)', stripeLocale: 'pt-PT', locale: 'pt-PT', + twoLetter: 'pt', }, { dateLocale: uk, @@ -145,5 +160,8 @@ export default [ englishName: 'Ukrainian', stripeLocale: 'uk-UA', locale: 'uk-UA', + twoLetter: 'uk', }, -] as Language[]; +]; + +export default languages; diff --git a/frontend/src/translations/locales/ar-SA.json b/frontend/src/translations/locales/ar-SA.json index ced332a96..05f7b45bb 100644 --- a/frontend/src/translations/locales/ar-SA.json +++ b/frontend/src/translations/locales/ar-SA.json @@ -155,7 +155,8 @@ "description": "سيؤدي هذا إلى حذف المنشور وجميع أصواته. لا يمكن التراجع عن هذا الإجراء.", "confirm": "حذف هذا المنشور", "cancel": "لقد غيرت رأيي" - } + }, + "demo": "مرحبا بكم في هذا العرض التجريبي 🎉 لا تتردد في اللعب وتغيير الإعدادات. يمكنك أيضًا إغلاق هذه الرسالة في أي وقت." }, "GameMenu": { "board": "المجلس", diff --git a/frontend/src/translations/locales/de-DE.json b/frontend/src/translations/locales/de-DE.json index e1ec5da22..e3eb0e15e 100644 --- a/frontend/src/translations/locales/de-DE.json +++ b/frontend/src/translations/locales/de-DE.json @@ -155,7 +155,8 @@ "description": "Dies löscht den Beitrag und alle seine Abstimmungen. Diese Aktion kann nicht rückgängig gemacht werden.", "confirm": "Diesen Beitrag löschen", "cancel": "Ich habe meine Meinung geändert" - } + }, + "demo": "Willkommen zu dieser Demo 🎉 Fühlen Sie sich frei zu spielen und Einstellungen zu ändern. Sie können diese Nachricht auch jederzeit schließen." }, "GameMenu": { "board": "Board", diff --git a/frontend/src/translations/locales/en-GB.json b/frontend/src/translations/locales/en-GB.json index 6859c012b..421fe2bb1 100644 --- a/frontend/src/translations/locales/en-GB.json +++ b/frontend/src/translations/locales/en-GB.json @@ -155,7 +155,8 @@ "description":"This will delete the post and all its votes. This action cannot be undone.", "confirm": "Delete this post", "cancel": "I have changed my mind" - } + }, + "demo": "Welcome to this demo 🎉 Feel free to play around and change settings. You can also close this message at any time." }, "GameMenu": { "board": "Board", diff --git a/frontend/src/translations/locales/es-ES.json b/frontend/src/translations/locales/es-ES.json index 78d72f7a6..8b3ab5518 100644 --- a/frontend/src/translations/locales/es-ES.json +++ b/frontend/src/translations/locales/es-ES.json @@ -155,7 +155,8 @@ "description": "Esto eliminará el mensaje y todos sus votos. Esta acción no se puede deshacer.", "confirm": "Eliminar esta publicación", "cancel": "He cambiado de opinión" - } + }, + "demo": "Bienvenido a esta demo 🎉 No dudes en jugar y cambiar la configuración. También puedes cerrar este mensaje en cualquier momento." }, "GameMenu": { "board": "Tablero", diff --git a/frontend/src/translations/locales/fr-FR.json b/frontend/src/translations/locales/fr-FR.json index 4835a5fe9..bc0a9ae56 100644 --- a/frontend/src/translations/locales/fr-FR.json +++ b/frontend/src/translations/locales/fr-FR.json @@ -155,7 +155,8 @@ "description": "Vous allez supprimer le post et tous ses votes. Cette action est définitive.", "confirm": "Supprimer ce post", "cancel": "J'ai changé d'avis" - } + }, + "demo": "Bienvenue dans cette démo 🎉 Faites comme chez vous et n'hésitez pas à modifier les réglages. Vous pouvez également fermer ce message à tout moment." }, "GameMenu": { "board": "Board", diff --git a/frontend/src/translations/locales/hu-HU.json b/frontend/src/translations/locales/hu-HU.json index 89a6d64a5..f03c66660 100644 --- a/frontend/src/translations/locales/hu-HU.json +++ b/frontend/src/translations/locales/hu-HU.json @@ -155,7 +155,8 @@ "description": "", "confirm": "", "cancel": "" - } + }, + "demo": "" }, "GameMenu": { "board": "Tábla", diff --git a/frontend/src/translations/locales/it-IT.json b/frontend/src/translations/locales/it-IT.json index c6ff0a0ff..93c6df072 100644 --- a/frontend/src/translations/locales/it-IT.json +++ b/frontend/src/translations/locales/it-IT.json @@ -155,7 +155,8 @@ "description": "Questo cancellerà il post e tutti i suoi voti. Questa azione non può essere annullata.", "confirm": "Elimina questo post", "cancel": "Ho cambiato idea" - } + }, + "demo": "Benvenuto in questa demo 🎉 Sentiti libero di giocare e modificare le impostazioni. Puoi anche chiudere questo messaggio in qualsiasi momento." }, "GameMenu": { "board": "Tavola", diff --git a/frontend/src/translations/locales/ja-JP.json b/frontend/src/translations/locales/ja-JP.json index 32da6c1f8..180e3a598 100644 --- a/frontend/src/translations/locales/ja-JP.json +++ b/frontend/src/translations/locales/ja-JP.json @@ -155,7 +155,8 @@ "description": "投稿とすべての投票を削除します。この操作は元に戻せません。", "confirm": "この投稿を削除", "cancel": "気が変わりました" - } + }, + "demo": "このデモへようこそ🎉 気軽にプレイして設定を変更してください。いつでもこのメッセージを閉じることができます。" }, "GameMenu": { "board": "ボード", diff --git a/frontend/src/translations/locales/nl-NL.json b/frontend/src/translations/locales/nl-NL.json index c1b5cfa25..4c0c843a2 100644 --- a/frontend/src/translations/locales/nl-NL.json +++ b/frontend/src/translations/locales/nl-NL.json @@ -155,7 +155,8 @@ "description": "Dit zal het bericht en al zijn stemmen verwijderen. Deze actie kan niet ongedaan worden gemaakt.", "confirm": "Dit bericht verwijderen", "cancel": "Ik ben van gedachten veranderd" - } + }, + "demo": "Welkom bij deze demo 🎉 Voel je vrij om te spelen en de instellingen aan te passen. Je kunt dit bericht op elk moment ook sluiten." }, "GameMenu": { "board": "Bord", diff --git a/frontend/src/translations/locales/pl-PL.json b/frontend/src/translations/locales/pl-PL.json index 7cba7d537..322bfad5c 100644 --- a/frontend/src/translations/locales/pl-PL.json +++ b/frontend/src/translations/locales/pl-PL.json @@ -155,7 +155,8 @@ "description": "Spowoduje to usunięcie wpisu i wszystkich jego głosów. Tej czynności nie można cofnąć.", "confirm": "Usuń ten post", "cancel": "Zmieniłem zdanie" - } + }, + "demo": "Witaj w tym demo 🎉 Możesz grać i zmieniać ustawienia. Możesz również zamknąć tę wiadomość w dowolnym momencie." }, "GameMenu": { "board": "Tablica", diff --git a/frontend/src/translations/locales/pt-BR.json b/frontend/src/translations/locales/pt-BR.json index ef6a8fda4..170a68d55 100644 --- a/frontend/src/translations/locales/pt-BR.json +++ b/frontend/src/translations/locales/pt-BR.json @@ -155,7 +155,8 @@ "description": "Isto irá excluir a publicação e todos os seus votos. Esta ação não pode ser desfeita.", "confirm": "Excluir esta publicação", "cancel": "Mudei de ideia" - } + }, + "demo": "Bem-vindo a esta demonstração 🎉 Sinta-se livre para reproduzir ao redor e alterar as configurações. Você também pode fechar esta mensagem a qualquer momento." }, "GameMenu": { "board": "Tabuleiro", diff --git a/frontend/src/translations/locales/pt-PT.json b/frontend/src/translations/locales/pt-PT.json index 231f5dab6..a1b510c0c 100644 --- a/frontend/src/translations/locales/pt-PT.json +++ b/frontend/src/translations/locales/pt-PT.json @@ -155,7 +155,8 @@ "description": "Isto irá excluir a publicação e todos os seus votos. Esta ação não pode ser desfeita.", "confirm": "Excluir esta publicação", "cancel": "Mudei de ideia" - } + }, + "demo": "Bem-vindo a esta demonstração 🎉 Sinta-se livre para reproduzir ao redor e alterar as configurações. Você também pode fechar esta mensagem a qualquer momento." }, "GameMenu": { "board": "Tabuleiro", diff --git a/frontend/src/translations/locales/uk-UA.json b/frontend/src/translations/locales/uk-UA.json index 9f422cf38..8eb998c5c 100644 --- a/frontend/src/translations/locales/uk-UA.json +++ b/frontend/src/translations/locales/uk-UA.json @@ -155,7 +155,8 @@ "description": "Ця дія видалить публікацію та всі її голоси. Цю дію не можна скасувати.", "confirm": "Видалити це повідомлення", "cancel": "Я передумала" - } + }, + "demo": "Ласкаво просимо на демо 🎉 Ви можете вільно грати і змінювати налаштування. Ви також можете закрити це повідомлення у будь-який час." }, "GameMenu": { "board": "Дошка", diff --git a/frontend/src/translations/locales/zh-CN.json b/frontend/src/translations/locales/zh-CN.json index 8d975bb11..d092b51aa 100644 --- a/frontend/src/translations/locales/zh-CN.json +++ b/frontend/src/translations/locales/zh-CN.json @@ -155,7 +155,8 @@ "description": "这将删除帖子及其所有投票。此操作无法撤消。", "confirm": "删除此帖子", "cancel": "我改变了主意。" - } + }, + "demo": "欢迎来到这个演示🎉 请随时随地播放并更改设置。您也可以随时关闭此消息。" }, "GameMenu": { "board": "棋盘", diff --git a/frontend/src/translations/locales/zh-TW.json b/frontend/src/translations/locales/zh-TW.json index d498f391b..6191f1a95 100644 --- a/frontend/src/translations/locales/zh-TW.json +++ b/frontend/src/translations/locales/zh-TW.json @@ -155,7 +155,8 @@ "description": "", "confirm": "", "cancel": "" - } + }, + "demo": "" }, "GameMenu": { "board": "木板", diff --git a/frontend/src/views/Demo.tsx b/frontend/src/views/Demo.tsx new file mode 100644 index 000000000..6936aca57 --- /dev/null +++ b/frontend/src/views/Demo.tsx @@ -0,0 +1,57 @@ +import styled from '@emotion/styled'; +import { colors } from '@mui/material'; +import { anonymousLogin, createDemoGame, me, updateLanguage } from 'api'; +import UserContext from 'auth/Context'; +import { useContext, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { trackEvent } from 'track'; +import { Language } from 'translations/languages'; +import { languages, useLanguage } from '../translations'; + +export default function Demo() { + const { setUser } = useContext(UserContext); + let [searchParams] = useSearchParams(); + const twoLetter = searchParams.get('lang'); + const [, changeLanguage] = useLanguage(); + const language = getLanguage(twoLetter || 'en'); + + useEffect(() => { + async function fetch() { + await anonymousLogin('Demo User'); + trackEvent('register/demo'); + let updatedUser = await me(); + if (updatedUser?.language === null) { + updatedUser = await updateLanguage(language.locale); + } + setUser(updatedUser); + changeLanguage(language.locale); + const session = await createDemoGame(); + if (session) { + window.location.href = `/game/${session.id}`; + } + } + fetch(); + }, [language.locale, setUser, changeLanguage]); + return ( + +

Hand tight...

+
+ ); +} + +function getLanguage(twoLetter: string): Language { + return languages.find((l) => l.twoLetter === twoLetter) || languages[0]; +} + +const Container = styled.div` + background-color: ${colors.deepPurple[400]}; + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - 60px); + + h1 { + color: white; + font-size: 3rem; + } +`; diff --git a/frontend/src/views/Game.tsx b/frontend/src/views/Game.tsx index d72a6e4a2..fb4132fa1 100644 --- a/frontend/src/views/Game.tsx +++ b/frontend/src/views/Game.tsx @@ -169,6 +169,7 @@ function GamePage() { void; onAddPost: (columnIndex: number, content: string, rank: string) => void; onAddGroup: (columnIndex: number, rank: string) => void; @@ -88,6 +91,7 @@ function GameMode({ columns, options, search, + demo, }: GameModeProps) { const { session } = useSession(); @@ -130,6 +134,11 @@ function GameMode({ return ( + {demo ? ( + + {t('PostBoard.demo')} + + ) : null} ({ messages: [], ready: [], timer: null, + demo: false, }); describe('Session Permission Logic', () => {