diff --git a/frontend/app/public/images/combatlog-fleet-card.jpg b/frontend/app/public/images/combatlog-fleet-card.jpg new file mode 100644 index 00000000..db7c7566 Binary files /dev/null and b/frontend/app/public/images/combatlog-fleet-card.jpg differ diff --git a/frontend/app/src/components/blocks/CombatLogAnalysisComponent.astro b/frontend/app/src/components/blocks/CombatLogAnalysisComponent.astro index a390830a..97aa3edd 100644 --- a/frontend/app/src/components/blocks/CombatLogAnalysisComponent.astro +++ b/frontend/app/src/components/blocks/CombatLogAnalysisComponent.astro @@ -18,6 +18,7 @@ const damage_out = combat_log_analysis?.damage_out const timeline_time = timeline.map(datetime => datetime.split(' ')[1]) import { format_date_time } from '@helpers/date' +import { get_item_icon } from '@helpers/eve_image_server' import Flexblock from '@components/compositions/Flexblock.astro' import Grid from '@components/compositions/Grid.astro' @@ -26,19 +27,63 @@ import TextGroup from '@components/blocks/TextGroup.astro' import ComponentBlock from '@components/blocks/ComponentBlock.astro' import DamageBadge from '@components/blocks/DamageBadge.astro'; import CombatLogChart from '@components/blocks/CombatLogChart.astro'; +import Badge from '@components/blocks/Badge.astro'; --- - - {format_date_time(lang, combat_log_analysis?.start)} - {format_date_time(lang, combat_log_analysis?.end)} - {combat_log_analysis?.logged_events.toLocaleString()} - + + + {format_date_time(lang, combat_log_analysis?.start)} + {format_date_time(lang, combat_log_analysis?.end)} + {combat_log_analysis?.logged_events.toLocaleString()} + {combat_log_analysis?.character_name} + {combat_log_analysis?.fitting && + + + + } + {(combat_log_analysis?.fleet_id ?? 0 > 0) && + {combat_log_analysis.fleet_id} + } + + + {(combat_log_analysis?.max_to || combat_log_analysis?.max_from) && + + {combat_log_analysis?.max_to && + + + {combat_log_analysis?.max_to.damage} - {combat_log_analysis?.max_to.weapon} - {combat_log_analysis?.max_to.outcome} + To {combat_log_analysis?.max_to.entity} + + + } + {combat_log_analysis?.max_from && + + + {combat_log_analysis?.max_from.damage} - {combat_log_analysis?.max_from.weapon} - {combat_log_analysis?.max_from.outcome} + From {combat_log_analysis?.max_from.entity} + + + } + + } + {(combat_log_analysis?.weapons?.length ?? 0) > 0 && diff --git a/frontend/app/src/components/blocks/CombatLogChart.astro b/frontend/app/src/components/blocks/CombatLogChart.astro index 818fbdc4..0e298bcf 100644 --- a/frontend/app/src/components/blocks/CombatLogChart.astro +++ b/frontend/app/src/components/blocks/CombatLogChart.astro @@ -26,8 +26,8 @@ import MultiRangeInput from '@components/blocks/MultiRangeInput.astro'; min: 0, max: 0, timeline_time: ${JSON.stringify(timeline_time)}, - damage_in: ${JSON.stringify(damage_in)}, - damage_out: ${JSON.stringify(damage_out)}, + damage_in: ${JSON.stringify(damage_in.map(damage => damage/10))}, + damage_out: ${JSON.stringify(damage_out.map(damage => damage/10))}, init() { const ctx = document.getElementById("damage-log-chart").getContext("2d") @@ -41,7 +41,7 @@ import MultiRangeInput from '@components/blocks/MultiRangeInput.astro'; labels: this.timeline_time, datasets: [ { - label: "${t('damage_taken')}", + label: "${t('dps_taken')}", data: this.damage_in, pointStyle: 'rect', backgroundColor: '#b53620', @@ -53,7 +53,7 @@ import MultiRangeInput from '@components/blocks/MultiRangeInput.astro'; } }, { - label: "${t('damage_done')}", + label: "${t('dps_done')}", data: this.damage_out, pointStyle: 'rect', backgroundColor: '#198754', @@ -86,7 +86,7 @@ import MultiRangeInput from '@components/blocks/MultiRangeInput.astro'; beginAtZero: true, title: { display: true, - text: "${t('damage')}" + text: "${t('damage_per_second')}" }, grid: { display: false diff --git a/frontend/app/src/components/blocks/DamageBadge.astro b/frontend/app/src/components/blocks/DamageBadge.astro index 1b1ffbb8..d21eda93 100644 --- a/frontend/app/src/components/blocks/DamageBadge.astro +++ b/frontend/app/src/components/blocks/DamageBadge.astro @@ -31,7 +31,7 @@ import Square from '@components/blocks/Square.astro' x-init="tippy($el, tippy_options)" data-tippy-content={t('damage_to_enemy')} /> - {damage.total_to.toLocaleString()} ({damage.dps_to.toLocaleString()} dps) - {damage.volleys_to.toLocaleString()} {t('volleys')} + {damage.total_to.toLocaleString()} ({damage.dps_to.toLocaleString()} {t('per_volley')}) - {damage.volleys_to.toLocaleString()} {t('volleys')} } {damage.volleys_from > 0 && @@ -41,7 +41,7 @@ import Square from '@components/blocks/Square.astro' x-init="tippy($el, tippy_options)" data-tippy-content={t('damage_from_enemy')} /> - {damage.total_from.toLocaleString()} ({damage.dps_from.toLocaleString()} dps) - {damage.volleys_from.toLocaleString()} {t('volleys')} + {damage.total_from.toLocaleString()} ({damage.dps_from.toLocaleString()} {t('per_volley')}) - {damage.volleys_from.toLocaleString()} {t('volleys')} } diff --git a/frontend/app/src/components/blocks/FittingCombatLogItem.astro b/frontend/app/src/components/blocks/FittingCombatLogItem.astro new file mode 100644 index 00000000..b3e8347e --- /dev/null +++ b/frontend/app/src/components/blocks/FittingCombatLogItem.astro @@ -0,0 +1,83 @@ +--- +import { i18n } from '@helpers/i18n' +const { t, lang, translatePath } = i18n(Astro.url) + +import type { SavedCombatLog, Fitting } from '@dtypes/api.minmatar.org' + +interface Props { + log: SavedCombatLog; + fitting: Fitting | undefined; +} + +const { + log, + fitting, +} = Astro.props + +const CAPSULE_TYPE_ID = 670 + +import { format_date_short } from '@helpers/date' + +import Flexblock from "@components/compositions/Flexblock.astro"; +import Wrapper from "@components/compositions/Wrapper.astro"; + +import ComponentBlock from '@components/blocks/ComponentBlock.astro'; +import ItemPicture from "@components/blocks/ItemPicture.astro"; +import ClipboardButton from "@components/blocks/ClipboardButton.astro"; +--- + + + + + + +

{fitting?.name ?? t('unknown_fitting_name')}

+ {format_date_short(lang, log.uploaded_at)} +
+
+ {fitting && + + {translatePath(`${Astro.url}/${log.id}`)} + + } +
+
+ + \ No newline at end of file diff --git a/frontend/app/src/components/blocks/FleetCombatLogItem.astro b/frontend/app/src/components/blocks/FleetCombatLogItem.astro new file mode 100644 index 00000000..25952578 --- /dev/null +++ b/frontend/app/src/components/blocks/FleetCombatLogItem.astro @@ -0,0 +1,98 @@ +--- +import { i18n } from '@helpers/i18n' +const { t, lang, translatePath } = i18n(Astro.url) + +import type { SavedCombatLog } from '@dtypes/api.minmatar.org' + +interface Props { + log: SavedCombatLog; +} + +const { + log, +} = Astro.props + +import { format_date_short } from '@helpers/date' + +import Flexblock from "@components/compositions/Flexblock.astro"; +import Wrapper from "@components/compositions/Wrapper.astro"; + +import ComponentBlock from '@components/blocks/ComponentBlock.astro'; +import ClipboardButton from "@components/blocks/ClipboardButton.astro"; +import FleetEvEIcon from '@components/icons/FleetEvEIcon.astro'; +--- + + + + + + +
+ + +

{t('fleet')} {log.fleet_id}

+ {format_date_short(lang, log.uploaded_at)} +
+
+ + {translatePath(`${Astro.url}/${log.id}`)} + +
+
+ + \ No newline at end of file diff --git a/frontend/app/src/components/blocks/FleetCombatLogListItem.astro b/frontend/app/src/components/blocks/FleetCombatLogListItem.astro new file mode 100644 index 00000000..0c60fea3 --- /dev/null +++ b/frontend/app/src/components/blocks/FleetCombatLogListItem.astro @@ -0,0 +1,34 @@ +--- +import { i18n } from '@helpers/i18n' +const { lang, t, translatePath } = i18n(Astro.url) + +import type { FleetCombatLog } from '@dtypes/layout_components' +import { format_date_time } from '@helpers/date' + +interface Props { + log: FleetCombatLog; +} + +const { + log, +} = Astro.props + +import PilotBadge from '@components/blocks/PilotBadge.astro'; +import StylessButton from './StylessButton.astro'; +--- + + + + {format_date_time(lang, log.uploaded_at)} + + + + \ No newline at end of file diff --git a/frontend/app/src/components/blocks/FleetDetails.astro b/frontend/app/src/components/blocks/FleetDetails.astro index ba50dd55..6bfed9ef 100644 --- a/frontend/app/src/components/blocks/FleetDetails.astro +++ b/frontend/app/src/components/blocks/FleetDetails.astro @@ -2,7 +2,7 @@ import { i18n } from '@helpers/i18n' const { lang, t, translatePath } = i18n(Astro.url) -import type { FleetUI, FleetCompositionUI, FleetRadarUI, SRPUI } from '@dtypes/layout_components' +import type { FleetUI, FleetCompositionUI, FleetRadarUI, SRPUI, FleetCombatLog } from '@dtypes/layout_components' interface Props { fleet: FleetUI; @@ -12,6 +12,7 @@ interface Props { fleet_composition?: FleetCompositionUI[]; fleet_radar?: FleetRadarUI[]; fleet_srps?: SRPUI[]; + saved_logs?: FleetCombatLog[]; } const { @@ -22,6 +23,7 @@ const { fleet_composition, fleet_radar, fleet_srps, + saved_logs = [], } = Astro.props import { query_string } from '@helpers/string'; @@ -36,6 +38,7 @@ import TextBox from '@components/layout/TextBox.astro'; import Flexblock from '@components/compositions/Flexblock.astro'; import FlexInline from '@components/compositions/FlexInline.astro'; +import Grid from '@components/compositions/Grid.astro'; import FleetCompositionBlock from '@components/blocks/FleetCompositionBlock.astro'; import Button from '@components/blocks/Button.astro'; @@ -45,6 +48,7 @@ import ComponentBlock from '@components/blocks/ComponentBlock.astro'; import PilotBadge from '@components/blocks/PilotBadge.astro'; import FleetStatus from '@components/blocks/FleetStatus.astro'; import SRPTable from '@components/blocks/SRPTable.astro'; +import FleetCombatLogListItem from '@components/blocks/FleetCombatLogListItem.astro'; const eve_time = new Date(fleet.start_time); const eve_time_text = eve_time.toLocaleDateString(lang, JSON.parse(import.meta.env.DATETIME_FORMAT)) @@ -197,4 +201,20 @@ const eve_time_text = eve_time.toLocaleDateString(lang, JSON.parse(import.meta.e + + {saved_logs?.length > 0 && + + + +

{t('fleet_logs')}

+ {saved_logs.length} {saved_logs.length !== 1 ? t('logs_recorded') : t('log_recorded')} +
+ + {saved_logs.map(log => + + )} + +
+
+ }
\ No newline at end of file diff --git a/frontend/app/src/components/blocks/FleetListItem.astro b/frontend/app/src/components/blocks/FleetListItem.astro index 1cd9260a..003c4b7e 100644 --- a/frontend/app/src/components/blocks/FleetListItem.astro +++ b/frontend/app/src/components/blocks/FleetListItem.astro @@ -42,7 +42,6 @@ import TextGroup from '@components/blocks/TextGroup.astro'; import MagnifierIcon from '@components/icons/buttons/MagnifierIcon.astro'; import StratopIcon from '@components/icons/StratopIcon.astro'; import NonStrategicIcon from '@components/icons/NonStrategicIcon.astro'; -import CasualIcon from '@components/icons/CasualIcon.astro'; import TrainingIcon from '@components/icons/TrainingIcon.astro'; import FlexInline from '@components/compositions/FlexInline.astro'; diff --git a/frontend/app/src/components/partials/HeadScripts.astro b/frontend/app/src/components/partials/HeadScripts.astro index 1737ca98..59a99ed4 100644 --- a/frontend/app/src/components/partials/HeadScripts.astro +++ b/frontend/app/src/components/partials/HeadScripts.astro @@ -37,6 +37,6 @@ - + \ No newline at end of file diff --git a/frontend/app/src/helpers/api.minmatar.org/combatlog.ts b/frontend/app/src/helpers/api.minmatar.org/combatlog.ts index 941ff714..27484cc3 100644 --- a/frontend/app/src/helpers/api.minmatar.org/combatlog.ts +++ b/frontend/app/src/helpers/api.minmatar.org/combatlog.ts @@ -1,8 +1,79 @@ -import type { CombatLog } from '@dtypes/api.minmatar.org' +import type { CombatLog, SavedCombatLog, SavedLogsRequest } from '@dtypes/api.minmatar.org' import { get_error_message, query_string } from '@helpers/string' const API_ENDPOINT = `${import.meta.env.API_URL}/api/combatlog` +export async function get_saved_logs(access_token:string, saved_logs_request:SavedLogsRequest) { + const headers = { + 'Content-Type': 'text/plain', + 'Authorization': `Bearer ${access_token}` + } + + const { user_id, fleet_id } = saved_logs_request + + const query_params = { + ...(user_id && { user_id }), + ...(fleet_id && { fleet_id }), + }; + + const query = query_string(query_params) + + const ENDPOINT = `${API_ENDPOINT}${query ? `?${query}` : '/'}` + + console.log(`Requesting GET: ${ENDPOINT}`) + + try { + const response = await fetch(ENDPOINT, { + headers: headers, + method: 'GET' + }) + + // console.log(response) + + if (!response.ok) { + throw new Error(get_error_message( + response.status, + `GET ${ENDPOINT}` + )) + } + + return await response.json() as SavedCombatLog[]; + } catch (error) { + throw new Error(`Error analizing log: ${error.message}`); + } +} + +export async function get_log_by_id(access_token:string, log_id:number) { + const headers = { + 'Content-Type': 'text/plain', + 'Authorization': `Bearer ${access_token}` + } + + const ENDPOINT = `${API_ENDPOINT}/${log_id}` + + console.log(`Requesting GET: ${ENDPOINT}`) + + try { + const response = await fetch(ENDPOINT, { + headers: headers, + method: 'GET' + }) + + // console.log(response) + + if (!response.ok) { + throw new Error(get_error_message( + response.status, + `GET ${ENDPOINT}` + )) + } + + return await response.json() as CombatLog; + } catch (error) { + throw new Error(`Error fetching log: ${error.message}`); + } +} + export async function analize_log(combatlog:string) { const headers = { 'Content-Type': 'text/plain' @@ -34,11 +105,15 @@ export async function analize_log(combatlog:string) { } } -export async function analize_zipped_log(combatlog:Uint8Array, fitting_id?:number, fleet_id?:number) { +export async function analize_zipped_log(combatlog:Uint8Array, access_token?:string, fitting_id?:number, fleet_id?:number) { const headers = { - 'Content-Type': 'application/gzip', + 'Content-Type': 'application/gzip' } + if (access_token) + headers['Authorization'] = `Bearer ${access_token}` + console.log(headers) + const optional_attributes = { ...((fitting_id !== undefined && fitting_id > 0) && { "fitting_id": fitting_id }), ...((fleet_id !== undefined && fleet_id > 0) && { "fleet_id": fleet_id }), diff --git a/frontend/app/src/helpers/fetching/combatlog.ts b/frontend/app/src/helpers/fetching/combatlog.ts index 7502e362..89e0227d 100644 --- a/frontend/app/src/helpers/fetching/combatlog.ts +++ b/frontend/app/src/helpers/fetching/combatlog.ts @@ -1,10 +1,78 @@ -import type { CombatLogAnalysis } from '@dtypes/layout_components' -import { analize_log, analize_zipped_log } from '@helpers/api.minmatar.org/combatlog' +import { useTranslations } from '@i18n/utils'; + +const t = useTranslations('en'); + +import type { CombatLogAnalysis, CombatLogMaxUI, FleetCombatLog } from '@dtypes/layout_components' +import { analize_log, analize_zipped_log, get_log_by_id, get_saved_logs } from '@helpers/api.minmatar.org/combatlog' import { generate_timeline } from '@helpers/date' import { parse_damage_from_logs } from '@helpers/eve' +import { get_fitting_by_id } from '@helpers/api.minmatar.org/ships' +import { unique_values } from '@helpers/array' +import { get_users_character } from '@helpers/fetching/characters' + +export async function fetch_combatlog_analysis(combatlog:string | Uint8Array, gzipped:boolean, access_token?:string, fitting_id?:number, fleet_id?:number) { + const analysis = gzipped ? await analize_zipped_log(combatlog as Uint8Array, access_token, fitting_id, fleet_id) : await analize_log(combatlog as string) + + const start_time = analysis.times[0].name + const end_time = analysis.times[analysis.times.length - 1].name + + const timeline = generate_timeline(start_time, end_time) + const damage_in:number[] = [] + const damage_out:number[] = [] + + const damage_time_in = {} + const damage_time_out = {} + analysis.times.forEach(tick => { + damage_time_in[tick.name] = tick.damage_from + damage_time_out[tick.name] = tick.damage_to + }) + + timeline.forEach(tick => { + damage_in.push(damage_time_in[tick] ?? 0) + damage_out.push(damage_time_out[tick] ?? 0) + }) + + const enemies = await parse_damage_from_logs(analysis.enemies) + const weapons = await parse_damage_from_logs(analysis.weapons) + + const max_from = analysis?.max_from ? { + damage: analysis?.max_from.damage, + entity: analysis?.max_from.entity, + outcome: analysis?.max_from.outcome, + weapon: analysis?.max_from.weapon, + } as CombatLogMaxUI : null + + const max_to = analysis?.max_to ? { + damage: analysis?.max_to.damage, + entity: analysis?.max_to.entity, + outcome: analysis?.max_to.outcome, + weapon: analysis?.max_to.weapon, + } as CombatLogMaxUI : null + + const fitting = analysis?.fitting_id > 0 ? await get_fitting_by_id(analysis?.fitting_id) : null + const fleet = analysis?.fleet_id > 0 ? analysis?.fleet_id : null + + return { + logged_events: analysis.logged_events, + damage_done: analysis.damage_done, + damage_taken: analysis.damage_taken, + start: new Date(analysis.start), + end: new Date(analysis.end), + damage_in: damage_in, + damage_out: damage_out, + timeline: timeline, + enemies: enemies, + weapons: weapons, + character_name: analysis.character_name, + ...(fitting && { fitting }), + ...(fleet_id && { fleet }), + ...(max_from && { max_from }), + ...(max_to && { max_to }), + } as CombatLogAnalysis +} -export async function fetch_combatlog_analysis(combatlog:string | Uint8Array, gzipped:boolean, fitting_id?:number, fleet_id?:number) { - const analysis = gzipped ? await analize_zipped_log(combatlog as Uint8Array, fitting_id, fleet_id) : await analize_log(combatlog as string) +export async function fetch_combatlog_by_id(access_token:string, log_id:number) { + const analysis = await get_log_by_id(access_token, log_id) const start_time = analysis.times[0].name const end_time = analysis.times[analysis.times.length - 1].name @@ -28,6 +96,23 @@ export async function fetch_combatlog_analysis(combatlog:string | Uint8Array, gz const enemies = await parse_damage_from_logs(analysis.enemies) const weapons = await parse_damage_from_logs(analysis.weapons) + const max_from = analysis?.max_from ? { + damage: analysis?.max_from.damage, + entity: analysis?.max_from.entity, + outcome: analysis?.max_from.outcome, + weapon: analysis?.max_from.weapon, + } as CombatLogMaxUI : null + + const max_to = analysis?.max_to ? { + damage: analysis?.max_to.damage, + entity: analysis?.max_to.entity, + outcome: analysis?.max_to.outcome, + weapon: analysis?.max_to.weapon, + } as CombatLogMaxUI : null + + const fitting = analysis?.fitting_id > 0 ? await get_fitting_by_id(analysis?.fitting_id) : null + const fleet_id = analysis?.fleet_id > 0 ? analysis?.fleet_id : null + return { logged_events: analysis.logged_events, damage_done: analysis.damage_done, @@ -39,5 +124,38 @@ export async function fetch_combatlog_analysis(combatlog:string | Uint8Array, gz timeline: timeline, enemies: enemies, weapons: weapons, + character_name: analysis.character_name, + ...(fitting && { fitting }), + ...(fleet_id && { fleet_id }), + ...(max_from && { max_from }), + ...(max_to && { max_to }), } as CombatLogAnalysis +} + +export async function get_fleet_combatlogs(access_token:string, fleet_id:number) { + const fleet_logs = await get_saved_logs(access_token as string, { fleet_id: fleet_id }) + const not_null_applications = fleet_logs.filter(fleet_log => fleet_log) + const user_ids = unique_values(not_null_applications.map(log => log.user_id)) + const loggers = user_ids.length > 0 ? await get_users_character(user_ids) : [] + + return fleet_logs.map(log => { + const logger = loggers.find(log => log.user_id === log.user_id) + + return { + id: log.id, + uploaded_at: log.uploaded_at, + user_id: log.user_id, + logger: logger !== undefined ? { + character_id: logger.character_id, + character_name: logger.character_name, + corporation: { + id: logger.corporation_id, + name: logger.corporation_name, + } + } : { + character_id: 0, + character_name: t('unknown_character') + } + } as FleetCombatLog + }) } \ No newline at end of file diff --git a/frontend/app/src/helpers/fetching/posts.ts b/frontend/app/src/helpers/fetching/posts.ts index 403fea30..bffc3820 100644 --- a/frontend/app/src/helpers/fetching/posts.ts +++ b/frontend/app/src/helpers/fetching/posts.ts @@ -3,7 +3,7 @@ import type { PostListUI, PostUI, Posts } from '@dtypes/layout_components' import type { PostRequest } from '@dtypes/api.minmatar.org' import { get_posts, get_post, get_posts_tags } from '@helpers/api.minmatar.org/posts' import { get_users_character, get_user_character } from '@helpers/fetching/characters' -import { paginate, unique_values } from '@helpers/array' +import { unique_values } from '@helpers/array' const t = useTranslations('en'); diff --git a/frontend/app/src/i18n/ui.ts b/frontend/app/src/i18n/ui.ts index a97abaff..e9ffdc95 100644 --- a/frontend/app/src/i18n/ui.ts +++ b/frontend/app/src/i18n/ui.ts @@ -1173,6 +1173,24 @@ export const ui = { 'share_post': 'Share post', 'post_url_copied': 'Post URL copied!', 'view_station_market_contracts': 'Contract service available on this station', + 'damage_per_second': 'Damage per second', + 'dps_taken': 'DPS taken', + 'dps_done': 'DPS done', + 'per_volley': 'per volley', + 'character_name': 'Character name', + 'max_damage_inflicted': 'Max damage inflicted', + 'max_damage_received': 'Max damage received', + 'fitting_logs': 'Fitting logs', + 'fleet_logs': 'Fleet logs', + 'unknown_fitting_name': 'Unknown fitting name', + 'share': 'Share', + 'get_combatlog_selects_error': 'An error occurred while fetching fleet and fitting options.', + 'get_combatlog_error': 'An error occurred while fetching the combatlog.', + 'invalid_combatlog': 'The Combat Log is not valid', + 'uploaded_at': 'Uploaded at', + 'combat_logs': 'Combat Logs', + 'logs_recorded': 'logs recorded', + 'log_recorded': 'log recorded', 'fetch_srp_error': 'An error ocurred fetching SRPs', 'copy_name': 'Copy name', 'copy_amount': 'Copy amount', diff --git a/frontend/app/src/pages/fleets/history/[fleet_id].astro b/frontend/app/src/pages/fleets/history/[fleet_id].astro index 6cdae050..864b9eb5 100644 --- a/frontend/app/src/pages/fleets/history/[fleet_id].astro +++ b/frontend/app/src/pages/fleets/history/[fleet_id].astro @@ -28,16 +28,18 @@ const fleet_id = parseInt(Astro.params.fleet_id ?? '0') if (isNaN(fleet_id)) return HTTP_404_Not_Found() -import type { FleetUI, SRPUI } from '@dtypes/layout_components' +import type { FleetUI, SRPUI, FleetCombatLog } from '@dtypes/layout_components' import type { EveCharacterProfile } from '@dtypes/api.minmatar.org' import { fetch_fleet_by_id } from '@helpers/fetching/fleets' import { fetch_fleet_srps } from '@helpers/fetching/srp' import { get_user_character } from '@helpers/fetching/characters' +import { get_fleet_combatlogs } from '@helpers/fetching/combatlog' let fetch_fleets_error:string | false = false let fleet:FleetUI | null = null let fleet_srps:SRPUI[] | null = [] let user_character:EveCharacterProfile | null = null +let saved_logs:FleetCombatLog[] = [] try { fleet = await fetch_fleet_by_id(auth_token as string, fleet_id) @@ -51,6 +53,12 @@ try { fetch_fleets_error = prod_error_messages() ? t('fetch_fleets_error') : error.message } +try { + saved_logs = user && !fetch_fleets_error ? await get_fleet_combatlogs(auth_token as string, fleet_id) : [] +} catch (error) { + console.log(error) +} + const can_remove_fleet = is_superuser || user_permissions.includes('fleets.delete_evefleet') || user_character?.character_id === fleet?.fleet_commander_id @@ -123,6 +131,7 @@ const page_title = `${t('fleet')} ${fleet_id}`; history={true} can_remove_fleet={can_remove_fleet} fleet_srps={fleet_srps} + saved_logs={saved_logs} /> } diff --git a/frontend/app/src/pages/intel/combatlog.astro b/frontend/app/src/pages/intel/combatlog.astro index b04f5f68..a5a09e3b 100644 --- a/frontend/app/src/pages/intel/combatlog.astro +++ b/frontend/app/src/pages/intel/combatlog.astro @@ -1,14 +1,23 @@ --- import { i18n } from '@helpers/i18n' -const { t } = i18n(Astro.url) +const { t, translatePath } = i18n(Astro.url) import { prod_error_messages } from '@helpers/env' +import { HTTP_404_Not_Found } from '@helpers/http_responses' + +import type { User } from '@dtypes/jwt' +import * as jose from 'jose' + +const auth_token = Astro.cookies.has('auth_token') ? (Astro.cookies.get('auth_token')?.value as string) : false +const user:User | false = auth_token ? jose.decodeJwt(auth_token) as User : false import type { SelectOptions } from '@dtypes/layout_components' -import type { Fitting } from '@dtypes/api.minmatar.org' +import type { Fitting, SavedCombatLog } from '@dtypes/api.minmatar.org' import { get_fittings } from '@helpers/api.minmatar.org/ships' import { get_fleets } from '@helpers/api.minmatar.org/fleets' +import { get_saved_logs } from '@helpers/api.minmatar.org/combatlog' +let saved_logs:SavedCombatLog[] = [] let fittings:Fitting[] = [] let fitting_options:SelectOptions[] = [] let combatlog_selects_error:string | false = false @@ -33,6 +42,14 @@ try { }) } catch (error) { combatlog_selects_error = prod_error_messages() ? t('get_fittings_error') : error.message + console.log(combatlog_selects_error) +} + +try { + saved_logs = user ? await get_saved_logs(auth_token as string, { user_id: user.user_id }) : [] +} catch (error) { + combatlog_selects_error = prod_error_messages() ? t('get_fittings_error') : error.message + console.log(combatlog_selects_error) } import Viewport from '@layouts/Viewport.astro'; @@ -41,11 +58,15 @@ import PageWide from '@components/page/PageWide.astro'; import PageTitle from '@components/page/PageTitle.astro'; import Flexblock from '@components/compositions/Flexblock.astro'; +import BlockList from '@components/compositions/BlockList.astro'; import FlexInline from '@components/compositions/FlexInline.astro'; import FixedFluid from '@components/compositions/FixedFluid.astro'; +import Grid from '@components/compositions/Grid.astro'; import AnalizeCombatLogButton from '@components/blocks/AnalizeCombatLogButton.astro'; import CollapsableButton from '@components/blocks/CollapsableButton.astro'; +import FittingCombatLogItem from '@components/blocks/FittingCombatLogItem.astro'; +import FleetCombatLogItem from '@components/blocks/FleetCombatLogItem.astro'; import TutorialIcon from '@components/icons/TutorialIcon.astro'; @@ -92,42 +113,69 @@ const page_description = t('intel.combatlog.leading_text');
- -

{t('paste_combatlog_hint')}

- - - + {saved_logs.length > 0 && + - - {t('paste_combatlog_tutorial')} - - - -
+ + {log.fitting_id > 0 ? + fitting.id === log.fitting_id)} /> : + + } + + )} + + } + + + {saved_logs.length === 0 && + - -
-
+

{t('paste_combatlog_hint')}

+ + + + + {t('paste_combatlog_tutorial')} + + + +
+ +
+ + }
\ No newline at end of file diff --git a/frontend/app/src/pages/intel/combatlog/[log_id].astro b/frontend/app/src/pages/intel/combatlog/[log_id].astro new file mode 100644 index 00000000..05f33531 --- /dev/null +++ b/frontend/app/src/pages/intel/combatlog/[log_id].astro @@ -0,0 +1,100 @@ +--- +import { i18n } from '@helpers/i18n' +const { t, translatePath } = i18n(Astro.url) +import { HTTP_404_Not_Found } from '@helpers/http_responses' + +import type { User } from '@dtypes/jwt' +import * as jose from 'jose' + +const auth_token = Astro.cookies.has('auth_token') ? (Astro.cookies.get('auth_token')?.value as string) : false +const user:User | false = auth_token ? jose.decodeJwt(auth_token) as User : false + +if (!auth_token || !user) + return HTTP_404_Not_Found() + +import { prod_error_messages } from '@helpers/env' + +import { fetch_combatlog_by_id } from '@helpers/fetching/combatlog' +import type { CombatLogAnalysis } from '@dtypes/layout_components' + +let get_combatlog_error:string | false = false +let combat_log_analysis:CombatLogAnalysis | null = null + +const log_id = parseInt(Astro?.params?.log_id ?? '0') + +try { + if (!(log_id > 0)) + throw new Error(t('invalid_combatlog')) + + combat_log_analysis = await fetch_combatlog_by_id(auth_token, log_id) +} catch (error) { + get_combatlog_error = prod_error_messages() ? t('get_combatlog_error') : error.message + console.log(get_combatlog_error) +} + +const COMBAT_LOG_PARTIAL_URL = translatePath(`/partials/combatlog_component?log_id=${log_id}`) + +import Viewport from '@layouts/Viewport.astro'; + +import PageWide from '@components/page/PageWide.astro'; +import PageTitle from '@components/page/PageTitle.astro'; + +import Flexblock from '@components/compositions/Flexblock.astro'; +import FlexInline from '@components/compositions/FlexInline.astro'; + +import CombatLogAnalysisComponent from '@components/blocks/CombatLogAnalysisComponent.astro' +import Button from '@components/blocks/Button.astro' +import ErrorRefetch from '@components/blocks/ErrorRefetch.astro'; + +const page_title = `${t('combatlog')} ${log_id}`; +--- + + + + + + + + {page_title} + + + + + + + + +
+ {log_id === 0 ? +

{t('invalid_combatlog')}

: + get_combatlog_error ? + + : + + } +
+
+
\ No newline at end of file diff --git a/frontend/app/src/pages/partials/combatlog_analysis_component.astro b/frontend/app/src/pages/partials/combatlog_analysis_component.astro index 9faa283a..494b2fcb 100644 --- a/frontend/app/src/pages/partials/combatlog_analysis_component.astro +++ b/frontend/app/src/pages/partials/combatlog_analysis_component.astro @@ -1,6 +1,13 @@ --- import { i18n } from '@helpers/i18n' const { t } = i18n(Astro.url) +import { HTTP_404_Not_Found } from '@helpers/http_responses' + +import type { User } from '@dtypes/jwt' +import * as jose from 'jose' + +const auth_token = Astro.cookies.has('auth_token') ? (Astro.cookies.get('auth_token')?.value as string) : false +const user:User | false = auth_token ? jose.decodeJwt(auth_token) as User : false import { prod_error_messages } from '@helpers/env' import { b64_to_Uint8Array } from '@helpers/string' @@ -19,7 +26,9 @@ if (Astro.request.method === "POST") { const base64_gziped_combatlog = form_data.get("gziped_combatlog")?.valueOf() const gziped_combatlog = b64_to_Uint8Array(base64_gziped_combatlog as string) - combat_log_analysis = await fetch_combatlog_analysis(gziped_combatlog, true, fitting_id, fleet_id) + combat_log_analysis = auth_token ? + await fetch_combatlog_analysis(gziped_combatlog, true, auth_token, fitting_id, fleet_id) : + await fetch_combatlog_analysis(gziped_combatlog, true) } catch (error) { combatlog_analysis_error = prod_error_messages() ? t('combatlog_analysis_error') : error.message } diff --git a/frontend/app/src/pages/partials/combatlog_component.astro b/frontend/app/src/pages/partials/combatlog_component.astro new file mode 100644 index 00000000..60cdabd9 --- /dev/null +++ b/frontend/app/src/pages/partials/combatlog_component.astro @@ -0,0 +1,53 @@ +--- +import { i18n } from '@helpers/i18n' +const { t, translatePath } = i18n(Astro.url) +import { HTTP_404_Not_Found } from '@helpers/http_responses' + +import type { User } from '@dtypes/jwt' +import * as jose from 'jose' + +const auth_token = Astro.cookies.has('auth_token') ? (Astro.cookies.get('auth_token')?.value as string) : false +const user:User | false = auth_token ? jose.decodeJwt(auth_token) as User : false + +if (!auth_token || !user) + return HTTP_404_Not_Found() + +import { prod_error_messages } from '@helpers/env' + +import { fetch_combatlog_by_id } from '@helpers/fetching/combatlog' +import type { CombatLogAnalysis } from '@dtypes/layout_components' + +let get_combatlog_error:string | false = false +let combat_log_analysis:CombatLogAnalysis | null = null + +const log_id = parseInt(Astro.url.searchParams.get('log_id') ?? '0') + +try { + if (!(log_id > 0)) + throw new Error(t('invalid_combatlog')) + + combat_log_analysis = await fetch_combatlog_by_id(auth_token, log_id) +} catch (error) { + get_combatlog_error = prod_error_messages() ? t('get_combatlog_error') : error.message + console.log(get_combatlog_error) +} + +const COMBAT_LOG_PARTIAL_URL = translatePath(`/partials/combatlog_component?log_id=${log_id}`) + +const delay = parseInt(Astro.url.searchParams.get('delay') ?? '0') + +import CombatLogAnalysisComponent from '@components/blocks/CombatLogAnalysisComponent.astro' +import ErrorRefetch from '@components/blocks/ErrorRefetch.astro'; +--- + +{get_combatlog_error ? + + : + +} \ No newline at end of file diff --git a/frontend/app/src/pages/partials/fleet_detail_component.astro b/frontend/app/src/pages/partials/fleet_detail_component.astro index 90f3b38f..46d01bfd 100644 --- a/frontend/app/src/pages/partials/fleet_detail_component.astro +++ b/frontend/app/src/pages/partials/fleet_detail_component.astro @@ -23,13 +23,14 @@ const fleet_id = parseInt(Astro.url.searchParams.get('fleet_id') as string) if (isNaN(fleet_id)) return HTTP_404_Not_Found() -import type { FleetUI, FleetCompositionUI, FleetRadarUI, SRPUI } from '@dtypes/layout_components' +import type { FleetUI, FleetCompositionUI, FleetRadarUI, SRPUI, FleetCombatLog } from '@dtypes/layout_components' import type { EveCharacterProfile } from '@dtypes/api.minmatar.org' import { fetch_fleet_by_id, group_members_by_ship, group_members_by_location } from '@helpers/fetching/fleets' import { get_fleet_members } from '@helpers/api.minmatar.org/fleets' import { get_user_character } from '@helpers/fetching/characters' import { fetch_fleet_srps } from '@helpers/fetching/srp' import { get_system_id } from '@helpers/sde/map' +import { get_fleet_combatlogs } from '@helpers/fetching/combatlog' let fetch_fleets_error:string | false = false let fleet:FleetUI | null = null @@ -38,6 +39,7 @@ let fleet_composition:FleetCompositionUI[] = [] let fleet_radar:FleetRadarUI[] = [] let fleet_srps:SRPUI[] | null = [] let is_fleet_commander = false +let saved_logs:FleetCombatLog[] = [] const upcoming = JSON.parse(Astro.url.searchParams.get('upcoming') as string) @@ -65,6 +67,12 @@ try { fetch_fleets_error = prod_error_messages() ? t('fetch_fleets_error') : error.message } +try { + saved_logs = user && !fetch_fleets_error ? await get_fleet_combatlogs(auth_token as string, fleet_id) : [] +} catch (error) { + console.log(error) +} + const FLEET_DETAIL_PARTIAL_URL = translatePath(`/partials/fleet_detail_component?fleet_id=${fleet_id}&upcoming=${JSON.stringify(upcoming)}`) const delay = parseInt(Astro.url.searchParams.get('delay') ?? '0') @@ -94,5 +102,6 @@ import FleetDetails from '@components/blocks/FleetDetails.astro'; fleet_radar={fleet_radar} history={!upcoming} fleet_srps={fleet_srps} + saved_logs={saved_logs} /> } \ No newline at end of file diff --git a/frontend/app/src/styles/_global.scss b/frontend/app/src/styles/_global.scss index c4b86e0c..92da5527 100644 --- a/frontend/app/src/styles/_global.scss +++ b/frontend/app/src/styles/_global.scss @@ -182,6 +182,10 @@ ul { color: var(--fleet-red); } +.text-green { + color: var(--green); +} + .text-status-1 { color: var(--security-status-1) } diff --git a/frontend/app/src/styles/_typography.scss b/frontend/app/src/styles/_typography.scss index 4df44f79..7f3342f6 100644 --- a/frontend/app/src/styles/_typography.scss +++ b/frontend/app/src/styles/_typography.scss @@ -45,4 +45,8 @@ img { textarea.uppercase:focus { text-transform: none !important; +} + +b { + font-weight: 600; } \ No newline at end of file diff --git a/frontend/app/src/types/api.minmatar.org.ts b/frontend/app/src/types/api.minmatar.org.ts index 5d1052c4..faaef3fe 100644 --- a/frontend/app/src/types/api.minmatar.org.ts +++ b/frontend/app/src/types/api.minmatar.org.ts @@ -377,8 +377,37 @@ export interface CombatLog { times: CombatLogItem[]; start: Date; end: Date; + db_id: number; + user_id: number; + fitting_id: number; + fleet_id: number; + character_name: string; + max_from: CombatLogMax; + max_to: CombatLogMax; +} + +export interface SavedCombatLog { + id: number; + uploaded_at: Date; + user_id: number; + fleet_id: number; + fitting_id: number; } +export interface CombatLogMax { + event_time: string; + damage: number; + direction: DamageDirection; + entity: string; + weapon: string; + outcome: string; + location: string; + text: string; +} + +export const damage_direction = [ 'from', 'to' ] as const +export type DamageDirection = typeof damage_direction[number] + export const combatlog_item_category = [ 'Weapon', 'Enemy', 'TimeBucket' ] as const export type CombatlogItemCategory = typeof combatlog_item_category[number] @@ -397,6 +426,11 @@ export interface CombatLogItem { last: Date; } +export interface SavedLogsRequest { + user_id?: number; + fleet_id?: number; +} + export interface Contract { expectation_id: number; title: string; diff --git a/frontend/app/src/types/layout_components.ts b/frontend/app/src/types/layout_components.ts index 00fcd500..ef57b7b7 100644 --- a/frontend/app/src/types/layout_components.ts +++ b/frontend/app/src/types/layout_components.ts @@ -883,6 +883,25 @@ export interface CombatLogAnalysis { timeline: string[]; damage_in: number[]; damage_out: number[]; + character_name: string; + fitting?: Fitting; + fleet_id?: number; + max_from?: CombatLogMaxUI; + max_to?: CombatLogMaxUI; +} + +export interface CombatLogMaxUI { + damage: number; + entity: string; + weapon: string; + outcome: string; +} + +export interface FleetCombatLog { + id: number; + uploaded_at: Date; + user_id: number; + logger: CharacterBasic; } export interface Damage {