ui-default: init
将ui-default合并至主存储库便于维护
No files matched your search
@@ -0,0 +1,13 @@
|
||||
.page--contest_detail
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
|
||||
.col--submit-at
|
||||
width: rem(150px)
|
||||
|
||||
.col--problem
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--submit-at
|
||||
display: none
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('contest_main', () => {
|
||||
// Contest Filter
|
||||
$('[name="filter-form"] [name="rule"]').change(() => {
|
||||
$('[name="filter-form"]').submit();
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,99 @@
|
||||
$highlight-button-color = #F6DF45
|
||||
|
||||
.page--contest_main
|
||||
.contest__title
|
||||
font-size: rem($font-size-title)
|
||||
line-height: 1.2em
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
.contest__date
|
||||
margin-right: rem(10px)
|
||||
white-space: nowrap
|
||||
|
||||
.contest__info-attended
|
||||
color: $success-color
|
||||
|
||||
.section--contest
|
||||
&.live
|
||||
background: #E98696
|
||||
background-image: -webkit-radial-gradient(left top, ellipse farthest-corner, #E98696 0%, #E96B6B 100%)
|
||||
|
||||
&.ready
|
||||
background: #469DCF
|
||||
background-image: -webkit-radial-gradient(left top, ellipse farthest-corner, #8ACDE6 0%, #469DCF 100%)
|
||||
|
||||
.status_title
|
||||
display: inline-block
|
||||
font-size: rem($font-size-secondary)
|
||||
color: #FFF
|
||||
background: rgba(#000, 0.3)
|
||||
padding: rem(7px 10px)
|
||||
border-radius: rem(2px)
|
||||
|
||||
h1
|
||||
line-height: 1.2
|
||||
font-size: rem(25px)
|
||||
margin: rem(20px 0)
|
||||
|
||||
.info
|
||||
color: #FFF
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
.icon
|
||||
color: #FFEB81
|
||||
font-size: rem($font-size-icon)
|
||||
|
||||
li
|
||||
display: inline-block
|
||||
margin-right: rem(10px)
|
||||
|
||||
.attandance
|
||||
margin-top: rem(15px)
|
||||
font-size: rem($font-size-secondary)
|
||||
opacity: 0.8
|
||||
|
||||
.detail-button
|
||||
cursor: pointer
|
||||
display: block
|
||||
text-align: center
|
||||
background: $highlight-button-color
|
||||
color: darken($highlight-button-color, 70%)
|
||||
line-height: rem(40px)
|
||||
border-radius: rem(20px)
|
||||
width: rem(150px)
|
||||
position: relative
|
||||
overflow: hidden
|
||||
transition: box-shadow .4s, background-color .4s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:after
|
||||
position: absolute
|
||||
background: #fff
|
||||
content: ""
|
||||
height: rem(100px)
|
||||
left: rem(-70px)
|
||||
top: rem(-25px)
|
||||
width: rem(50px)
|
||||
opacity: 0.2
|
||||
transform: rotate(35deg)
|
||||
transition: left 0.6s ease-out-cubic
|
||||
z-index: 1
|
||||
|
||||
&:hover
|
||||
text-decoration: none
|
||||
box-shadow: 0 2px 20px rgba($highlight-button-color, 0.8)
|
||||
background: lighten($highlight-button-color, 20%)
|
||||
|
||||
&:after
|
||||
left: 150%
|
||||
|
||||
+mobile()
|
||||
.media, .media_body, .media__right
|
||||
display: block
|
||||
padding: 0
|
||||
|
||||
.media__right .section__body
|
||||
padding-top: 0
|
||||
|
||||
.detail-button
|
||||
width: auto
|
||||
@@ -0,0 +1,22 @@
|
||||
.page--contest_scoreboard
|
||||
.col--rank
|
||||
width: rem(70px)
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--user
|
||||
width: rem(140px)
|
||||
|
||||
.col--total_score, .col--total_time_str, .col--solved_problems
|
||||
width: rem(100px)
|
||||
text-align: center
|
||||
|
||||
.col--problem_detail
|
||||
border-left: 1px solid $table-border-color
|
||||
text-align: center
|
||||
|
||||
+mobile()
|
||||
.col--user
|
||||
width: auto
|
||||
|
||||
.col--problem_detail
|
||||
display: none
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('discussion_detail', async () => {
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,20 @@
|
||||
.page--discussion_detail
|
||||
|
||||
.topic__content
|
||||
margin: rem(20px 0)
|
||||
|
||||
.profile__bg
|
||||
background-size: cover
|
||||
background-position: center center
|
||||
height: rem(60px)
|
||||
margin-bottom: rem(-32px)
|
||||
|
||||
.sidebar-user-profile
|
||||
.media__body
|
||||
padding-left: rem(10px)
|
||||
padding-top: rem(20px)
|
||||
|
||||
.user-profile-name
|
||||
font-size: 1.2rem
|
||||
color: #444
|
||||
vertical-align: middle
|
||||
@@ -0,0 +1,37 @@
|
||||
.page--discussion_main,
|
||||
.page--discussion_node,
|
||||
.page--user_detail
|
||||
.discussion__item
|
||||
border-left: rem(5px) solid transparent
|
||||
padding-left: rem(5px)
|
||||
|
||||
&.highlight
|
||||
border-left: rem(5px) solid $highlight-color
|
||||
|
||||
.discussion__title
|
||||
font-size: rem($font-size-title)
|
||||
line-height: 1.2em
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
a:visited
|
||||
color: lighten($text-1-color, 60%)
|
||||
|
||||
.discussion__replies
|
||||
width: rem(70px)
|
||||
|
||||
.discussion-node__bg
|
||||
height: 0
|
||||
box-sizing: content-box
|
||||
padding-bottom: 40%
|
||||
background-size: cover
|
||||
background-position: left center
|
||||
background-repeat: no-repeat
|
||||
|
||||
for id in 'qa' 'share' 'vijos' 'solution' 'advice'
|
||||
&.{id}
|
||||
background-image: url('./discussion_node_bg/nodes_' + id + '.png')
|
||||
+retina()
|
||||
background-image: url('./discussion_node_bg/nodes@2x_' + id + '.png')
|
||||
|
||||
+below(rupture.desktop-cutoff)
|
||||
display: none
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('discussion_node', async () => {
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,8 @@
|
||||
.page--domain_join
|
||||
.domain_join__container
|
||||
+above(rupture.mobile-cutoff)
|
||||
width: rem(500px)
|
||||
margin: rem(50px) auto
|
||||
|
||||
+above(rupture.desktop-cutoff)
|
||||
width: rem(700px)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import * as domainEnum from 'vj/constant/domain';
|
||||
|
||||
const page = new NamedPage('domain_join_applications', () => {
|
||||
const $role = $('[name="role"]');
|
||||
const $expire = $('[name="expire"]');
|
||||
const $code = $('[name="invitationCode"]');
|
||||
function updateFormState() {
|
||||
const method = parseInt($('[name="method"]').val(), 10);
|
||||
$role.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
|
||||
$expire.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
|
||||
$code.prop('disabled', method !== domainEnum.JOIN_METHOD_CODE).trigger('vjFormDisableUpdate');
|
||||
}
|
||||
updateFormState();
|
||||
$('[name="method"]').change(() => updateFormState());
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,14 @@
|
||||
.page--domain_permission
|
||||
.col--p
|
||||
width: 80px
|
||||
text-align: center
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
td.col--description
|
||||
padding-left: rem(30px) !important
|
||||
|
||||
.col--family
|
||||
line-height: 2
|
||||
color: #FFF
|
||||
background: lighten(saturate($primary-color, 70%), 30%)
|
||||
border-top: 2px solid $primary-color
|
||||
@@ -0,0 +1,86 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog';
|
||||
|
||||
import request from 'vj/utils/request';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
const page = new NamedPage('domain_role', () => {
|
||||
const createRoleDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--create-role > div'),
|
||||
onDispatch(action) {
|
||||
const $role = createRoleDialog.$dom.find('[name="role"]');
|
||||
if (action === 'ok' && $role.val() === '') {
|
||||
$role.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
createRoleDialog.clear = function () {
|
||||
this.$dom.find('[name="role"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
function ensureAndGetSelectedRoles() {
|
||||
const roles = _.map(
|
||||
$('.domain-roles tbody [type="checkbox"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-role'),
|
||||
);
|
||||
if (roles.length === 0) {
|
||||
Notification.error(i18n('Please select at least one role to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
async function handleClickCreateRole() {
|
||||
const action = await createRoleDialog.clear().open();
|
||||
if (action !== 'ok') {
|
||||
return;
|
||||
}
|
||||
const role = createRoleDialog.$dom.find('[name="role"]').val();
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'add',
|
||||
role,
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickDeleteSelected() {
|
||||
const selectedRoles = ensureAndGetSelectedRoles();
|
||||
if (selectedRoles === null) return;
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm deleting the selected roles?')}</p>
|
||||
<p>${i18n('Users with those roles will be removed from the domain.')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'delete',
|
||||
roles: selectedRoles,
|
||||
});
|
||||
Notification.success(i18n('Selected roles have been deleted.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
$('[name="create_role"]').click(() => handleClickCreateRole());
|
||||
$('[name="delete_selected"]').click(() => handleClickDeleteSelected());
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,6 @@
|
||||
.page--domain_role
|
||||
.col--checkbox
|
||||
width: 60px
|
||||
|
||||
.col--users
|
||||
width: 100px
|
||||
@@ -0,0 +1,157 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
|
||||
import request from 'vj/utils/request';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
const page = new NamedPage('domain_user', () => {
|
||||
const addUserSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--add-user [name="user"]'));
|
||||
const addUserDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--add-user > div'),
|
||||
onDispatch(action) {
|
||||
const $role = addUserDialog.$dom.find('[name="role"]');
|
||||
if (action === 'ok') {
|
||||
if (addUserSelector.value() === null) {
|
||||
addUserSelector.focus();
|
||||
return false;
|
||||
}
|
||||
if ($role.val() === '') {
|
||||
$role.focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
addUserDialog.clear = function () {
|
||||
addUserSelector.clear();
|
||||
this.$dom.find('[name="role"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
const setRolesDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--set-role > div'),
|
||||
onDispatch(action) {
|
||||
const $role = setRolesDialog.$dom.find('[name="role"]');
|
||||
if (action === 'ok' && $role.val() === '') {
|
||||
$role.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
setRolesDialog.clear = function () {
|
||||
this.$dom.find('[name="role"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
async function handleClickAddUser() {
|
||||
const action = await addUserDialog.clear().open();
|
||||
if (action !== 'ok') {
|
||||
return;
|
||||
}
|
||||
const user = addUserSelector.value();
|
||||
const role = addUserDialog.$dom.find('[name="role"]').val();
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'set_user',
|
||||
uid: user._id,
|
||||
role,
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAndGetSelectedUsers() {
|
||||
const users = _.map(
|
||||
$('.domain-users tbody [type="checkbox"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-uid'),
|
||||
);
|
||||
if (users.length === 0) {
|
||||
Notification.error(i18n('Please select at least one user to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
async function handleClickRemoveSelected() {
|
||||
const selectedUsers = ensureAndGetSelectedUsers();
|
||||
if (selectedUsers === null) {
|
||||
return;
|
||||
}
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm removing the selected users?')}</p>
|
||||
<p>${i18n('Their account will not be deleted and they will be with the default role.')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'set_users',
|
||||
uid: selectedUsers,
|
||||
role: 'default',
|
||||
});
|
||||
Notification.success(i18n('Selected users have been removed from the domain.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickSetSelected() {
|
||||
const selectedUsers = ensureAndGetSelectedUsers();
|
||||
if (selectedUsers === null) {
|
||||
return;
|
||||
}
|
||||
const action = await setRolesDialog.clear().open();
|
||||
if (action !== 'ok') {
|
||||
return;
|
||||
}
|
||||
const role = setRolesDialog.$dom.find('[name="role"]').val();
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'set_users',
|
||||
uid: selectedUsers,
|
||||
role,
|
||||
});
|
||||
Notification.success(i18n('Role has been updated to {0} for selected users.', role));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangeUserRole(ev) {
|
||||
const row = $(ev.currentTarget).closest('tr');
|
||||
const role = $(ev.currentTarget).val();
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'set_user',
|
||||
uid: row.attr('data-uid'),
|
||||
role,
|
||||
});
|
||||
Notification.success(i18n('Role has been updated to {0}.', role));
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
$('[name="add_user"]').click(() => handleClickAddUser());
|
||||
$('[name="remove_selected"]').click(() => handleClickRemoveSelected());
|
||||
$('[name="set_roles"]').click(() => handleClickSetSelected());
|
||||
$('.domain-users [name="role"]').change((ev) => handleChangeUserRole(ev));
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,9 @@
|
||||
.page--domain_user
|
||||
.col--checkbox
|
||||
width: 60px
|
||||
|
||||
.col--uid
|
||||
width: 100px
|
||||
|
||||
.col--role
|
||||
width: 160px
|
||||
@@ -0,0 +1,44 @@
|
||||
$error_width = 650px
|
||||
$twd2_width = 270px
|
||||
|
||||
.page--error
|
||||
.error__icon-container,
|
||||
.error__text-container
|
||||
text-align: center
|
||||
margin: rem(20px 0)
|
||||
|
||||
.error__text-container
|
||||
color: #777
|
||||
line-height: 1.5
|
||||
|
||||
h1
|
||||
font-size: 4rem
|
||||
color: #999
|
||||
margin-bottom: rem(20px)
|
||||
|
||||
li
|
||||
list-style: decimal inside
|
||||
|
||||
.error__twd2
|
||||
background: url('error/twd2.svg') no-repeat
|
||||
width: $twd2_width
|
||||
height: $twd2_width
|
||||
display: inline-block
|
||||
|
||||
+above(rupture.mobile-cutoff)
|
||||
.error__container
|
||||
width: rem(700px)
|
||||
margin: rem(50px) auto
|
||||
position: relative
|
||||
|
||||
.error__icon-container
|
||||
float: left
|
||||
width: $twd2_width
|
||||
|
||||
.error__text-container
|
||||
left: rem($twd2_width + 50px)
|
||||
position: absolute
|
||||
top: 50%
|
||||
transform: translateY(-50%)
|
||||
text-align: left
|
||||
margin: 0
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 270">
|
||||
<g>
|
||||
<g transform="translate(0 20)">
|
||||
<path fill="#285D8F" d="M211.6 51c6.8-7.8 15-11 24.2-7.5 8.3 3 13 9.8 12 19 17 12.8 18.3 19 7.6 38.5 6 6 10.7 12.8 9.8 22.3-.2 2.3 1.7 4.8 2.6 7.2 5.2 15.4-4.6 28.2-20.6 27l-16.3-1.7c5 5.4 9 9.2 12.5 13.3 8.4 9.6 10 17.4 5.4 25.5-4.7 8.2-12.5 11.2-24.2 9.4l-6-1.5c0 2.8.2 5.2.5 7.6 1.2 9.2 0 17.4-8.6 22.6-7.7 4.7-19.5 3.3-27-3.2-3.6-3-6.6-7-10-10.6-5.5 2.4-5 7.7-6 12.2-3 13-7.2 17.6-17.2 19-9.5 1-15-2.8-20.8-14.7-.3-.6-1-1-2-2-6.5 7.6-13.2 15-24.2 15-9.4 0-15.2-6.3-18.4-20l-9.7-5.4c-13.4 8.5-22.2 10-34-6.2-9-1-18-2.2-21.5-12.6-4-11.3 1.6-19.3 12.4-26.8l-12.5 1C6 178.5-3.3 166 1 153c2.2-6.7 5.3-13 7.5-19.7 4-12 5-24.2-.8-36.3-1.7-3.5-3.4-8.7-2-11.6 3.7-7.8 9.2-14.7 13.6-22 5-8.6 12.6-11 20.2-9.6 7.2-10 13-19.3 20.3-27.4 3.4-3.7 9.5-5 16.4-8.2C88.7 3 98.8 3.7 114 23.4c8.5-9.8 10.2-10.4 26.8-9.3C145.4 3.6 154-1.7 165.5.8c9.6 2 14 10.2 15.2 20.7 9.7-4.6 19.4-7.6 27.5 1.5 7.3 8 5.3 17.7 3.4 28zM186 90.8c-17.5-.2-31 12.7-30.8 29.6 0 17.3 13.3 31 30 31 16.6 0 29.6-13.5 30-30.8.4-14-13-31.7-29.3-29.8zM84 88.6c-17.7-.2-30.7 12-31 29.3 0 17.2 13 31 29.6 31.2 16.7.2 30.3-13.4 30.4-30.4.2-17-12.4-30-29-30.2z"/>
|
||||
<path fill="#549FC4" d="M258 71.4c1.6 6.7 1.8 9.6 1.8 11.7 0 2.5-.7 3.8-4 17 7.6-13.8 8.8-20.8 2-28.6zM232.5 156l14.6 1.4c16 1.3 26-11.4 20.6-27-.8-2.3-2.8-4.8-2.6-7 .7-7.3-1.8-13-5.7-17.8 2 3.7 3 7.8 2.4 12.7-.3 2.3-.4 3.6 1.2 6.7 9 17.6-5.7 28.2-10.6 30-8.4 2.8-20 1-20 1zm13 33.5c-4.6 8.2-12.4 11.2-24 9.4-2-.4-3.7-1-6.2-1.5.2 2.8.3 5.2.6 7.6 1.2 9.2 0 17.4-8.6 22.6-7.7 4.7-19.5 3.3-27-3.2-3.6-3-6.6-7-10-10.6-5.5 2.4-5 7.7-6 12.2-3 13-7.2 17.6-17.2 19-6.8.8-11.6-1-16-6.6 5.4 9.5 10.6 12.6 19.2 11.5 10-1.3 14.2-6 17-19 1-4.4.7-9.8 6.2-12.2 3.4 3.6 6.4 7.5 10 10.6 7.5 6.5 19.3 7.8 27 3.2 8.5-5.2 10-13.4 8.6-22.5-.3-2.3-.4-4.8-.6-7.5l6 1.4c11.8 1.8 19.6-1.3 24.2-9.5 4-7.3 3.2-14.3-3-22.5 3.2 6 3 11.7-.2 17.5z"/>
|
||||
<g transform="translate(190 122)">
|
||||
<circle r="16" fill="#111">
|
||||
<animateTransform attributeName="transform" type="scale" calcMode="linear" values="1 1; 1 0; 1 1" begin="3s; loop.end + 3s" dur="0.15s" />
|
||||
</circle>
|
||||
</g>
|
||||
<g transform="translate(87 120)">
|
||||
<circle r="16" fill="#111">
|
||||
<animateTransform attributeName="transform" type="scale" calcMode="linear" values="1 1; 1 0; 1 1" begin="3s; loop.end + 3s" dur="0.15s" />
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<rect id="looper" x="0" y="0" width="1" height="1" style="fill: none; stroke: none">
|
||||
<set id="loop" attributeName="x" to="0" begin="0s; loop.end" dur="8s" fill="freeze"/>
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,5 @@
|
||||
.page--home_account
|
||||
[name="form_item_background_img"] .radiobox__image
|
||||
width: rem(100px)
|
||||
height: rem(62px)
|
||||
background-size: rem(100px 62px)
|
||||
@@ -0,0 +1,18 @@
|
||||
.page--home_domain
|
||||
.col--icon
|
||||
width: 60px
|
||||
line-height: 1
|
||||
|
||||
img
|
||||
border-radius: 50%
|
||||
|
||||
.col--role
|
||||
width: rem(160px)
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
.col--action
|
||||
width: rem(135px)
|
||||
|
||||
+mobile()
|
||||
.col--icon, .col--role
|
||||
display: none
|
||||
@@ -0,0 +1,103 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import request from 'vj/utils/request';
|
||||
import loadReactRedux from 'vj/utils/loadReactRedux';
|
||||
import parseQueryString from 'vj/utils/parseQueryString';
|
||||
|
||||
import { ActionDialog } from 'vj/components/dialog';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
|
||||
const page = new NamedPage('home_messages', () => {
|
||||
let reduxStore;
|
||||
|
||||
function createDialog(user) {
|
||||
reduxStore.dispatch({
|
||||
type: 'DIALOGUES_CREATE',
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
reduxStore.dispatch({
|
||||
type: 'DIALOGUES_SWITCH_TO',
|
||||
payload: user._id,
|
||||
});
|
||||
}
|
||||
|
||||
async function mountComponent() {
|
||||
const { default: SockJs } = await import('../components/socket');
|
||||
const { default: MessagePadApp } = await import('../components/messagepad');
|
||||
const { default: MessagePadReducer } = await import('../components/messagepad/reducers');
|
||||
const {
|
||||
React, render, Provider, store,
|
||||
} = await loadReactRedux(MessagePadReducer);
|
||||
|
||||
reduxStore = store;
|
||||
|
||||
const sock = new SockJs('/home/messages-conn');
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
store.dispatch({
|
||||
type: 'DIALOGUES_MESSAGE_PUSH',
|
||||
payload: msg,
|
||||
});
|
||||
};
|
||||
|
||||
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]'));
|
||||
const userSelectDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--user-select > div'),
|
||||
onDispatch(action) {
|
||||
if (action === 'ok' && userSelector.value() === null) {
|
||||
userSelector.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
userSelectDialog.clear = function () {
|
||||
userSelector.clear();
|
||||
return this;
|
||||
};
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<MessagePadApp
|
||||
onAdd={async () => {
|
||||
const action = await userSelectDialog.clear().open();
|
||||
if (action !== 'ok') {
|
||||
return;
|
||||
}
|
||||
const user = userSelector.value();
|
||||
createDialog(user);
|
||||
}}
|
||||
/>
|
||||
</Provider>,
|
||||
$('#messagePad').get(0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A target user id may be assigned in the query string.
|
||||
*/
|
||||
async function loadSendTarget() {
|
||||
const queryString = parseQueryString();
|
||||
if (!queryString.target) {
|
||||
return;
|
||||
}
|
||||
const user = await request.get('/user/search', {
|
||||
q: queryString.target,
|
||||
exact_match: true,
|
||||
});
|
||||
if (!user || user.length === 0) {
|
||||
return;
|
||||
}
|
||||
createDialog(user[0]);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await mountComponent();
|
||||
await loadSendTarget();
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,31 @@
|
||||
.page--home_security
|
||||
|
||||
.sessionlist
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
.sessionlist__icon
|
||||
font-size: rem(30px)
|
||||
color: #888
|
||||
margin: rem(0 20px)
|
||||
|
||||
.sessionlist__item
|
||||
margin: rem(10px 0)
|
||||
padding: rem(10px 0)
|
||||
border-bottom: 1px solid #DDD
|
||||
|
||||
input
|
||||
margin: 0
|
||||
|
||||
.sessionlist__current-session
|
||||
line-height: rem($form-control-height)
|
||||
color: green
|
||||
|
||||
+mobile()
|
||||
.sessionlist__item
|
||||
.media__left
|
||||
display: none
|
||||
|
||||
.media__body, .media__right
|
||||
display: block
|
||||
padding: 0
|
||||
margin: 0
|
||||
@@ -0,0 +1,13 @@
|
||||
.page--homework_detail
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
|
||||
.col--submit-at
|
||||
width: rem(150px)
|
||||
|
||||
.col--problem
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--submit-at
|
||||
display: none
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Calendar from 'vj/components/calendar';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import { parse as parseMongoId } from 'vj/utils/mongoId';
|
||||
|
||||
const page = new NamedPage('homework_main', () => {
|
||||
// Homework Calendar
|
||||
if (UiContext.docs) {
|
||||
const events = UiContext.docs.map((doc) => ({
|
||||
beginAt: doc.beginAt,
|
||||
endAt: doc.endAt,
|
||||
title: doc.title,
|
||||
maskFrom: doc.penaltySince ? doc.penaltySince : null,
|
||||
maskTitle: i18n('Time Extension'),
|
||||
colorIndex: parseMongoId(doc._id).timestamp % 12,
|
||||
link: doc.url,
|
||||
}));
|
||||
const calendar = new Calendar(events);
|
||||
calendar.getDom().appendTo('[name="calendar_entry"]');
|
||||
$('[name="homework_display"]').change((ev) => {
|
||||
switch (ev.currentTarget.value) {
|
||||
case 'calendar':
|
||||
$('.homework__list').hide();
|
||||
$('[name="calendar_entry"]').show();
|
||||
break;
|
||||
case 'list':
|
||||
$('.homework__list').show();
|
||||
$('[name="calendar_entry"]').hide();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected display parameter');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,14 @@
|
||||
$highlight-button-color = #F6DF45
|
||||
|
||||
.page--homework_main
|
||||
.homework__title
|
||||
line-height: 1.2em
|
||||
margin-bottom: rem(10px)
|
||||
font-size: rem($font-size-title)
|
||||
|
||||
.homework__info-attended
|
||||
color: $success-color
|
||||
|
||||
.homework__date
|
||||
margin-right: rem(10px)
|
||||
white-space: nowrap
|
||||
@@ -0,0 +1,22 @@
|
||||
.page--homework_scoreboard
|
||||
.col--rank
|
||||
width: rem(70px)
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--user
|
||||
width: rem(140px)
|
||||
|
||||
.col--total_score, .col--total_time_str, .col--solved_problems
|
||||
width: rem(100px)
|
||||
text-align: center
|
||||
|
||||
.col--problem_detail
|
||||
border-left: 1px solid $table-border-color
|
||||
text-align: center
|
||||
|
||||
+mobile()
|
||||
.col--user
|
||||
width: auto
|
||||
|
||||
.col--problem_detail
|
||||
display: none
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('manage_dashboard', async () => {
|
||||
const { default: SockJs } = await import('../components/socket');
|
||||
|
||||
const sock = new SockJs('/manage/check-conn');
|
||||
sock.onopen = () => {
|
||||
$('<blockquote class="blue"><p>Connection opened.</p></blockquote>').appendTo('#messages');
|
||||
};
|
||||
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
if (msg.type === 'log') {
|
||||
const div = $('<blockquote class="blue">').appendTo('#messages');
|
||||
$(`<p>${msg.payload}</p>`).appendTo(div);
|
||||
} else if (msg.type === 'warn') {
|
||||
const div = $('<blockquote class="yellow">').appendTo('#messages');
|
||||
$(`<p>${msg.payload}</p>`).appendTo(div);
|
||||
} else if (msg.type === 'error') {
|
||||
const div = $('<blockquote class="red">').appendTo('#messages');
|
||||
$(`<p>${msg.payload}</p>`).appendTo(div);
|
||||
}
|
||||
};
|
||||
|
||||
sock.onclose = (message) => {
|
||||
$(`
|
||||
<blockquote class="yellow">
|
||||
<p>Connection closed,reason=${JSON.stringify(message.reason)}</p>
|
||||
</blockquote>
|
||||
`).appendTo('#messages');
|
||||
};
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ActionDialog } from 'vj/components/dialog';
|
||||
|
||||
import request from 'vj/utils/request';
|
||||
|
||||
const page = new NamedPage('manage_script', () => {
|
||||
const runScriptDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--run-script > div'),
|
||||
});
|
||||
runScriptDialog.clear = function () {
|
||||
this.$dom.find('[name="args"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
window.runScript = async function (id) {
|
||||
const action = await runScriptDialog.clear().open();
|
||||
if (action !== 'ok') return;
|
||||
const args = runScriptDialog.$dom.find('[name="args"]').val();
|
||||
try {
|
||||
const res = await request.post('', {
|
||||
args,
|
||||
id,
|
||||
});
|
||||
window.location.href = res.url;
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import request from 'vj/utils/request';
|
||||
import delay from 'vj/utils/delay';
|
||||
|
||||
const page = new NamedPage('manage_user_import', () => {
|
||||
async function post(draft) {
|
||||
try {
|
||||
const res = await request.post('', {
|
||||
users: $('[name="users"]').val(),
|
||||
draft,
|
||||
});
|
||||
if (!draft) {
|
||||
Notification.success(i18n('Created {0} users.', res.users.length));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} else {
|
||||
$('[name="messages"]').text(res.messages.join('\n'));
|
||||
}
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
$('[name="preview"]').click(() => post(true));
|
||||
$('[name="submit"]').click(() => post(false));
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Navigation from 'vj/components/navigation';
|
||||
import { ActionDialog } from 'vj/components/dialog';
|
||||
import loadReactRedux from 'vj/utils/loadReactRedux';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import request from 'vj/utils/request';
|
||||
|
||||
class ProblemPageExtender {
|
||||
constructor() {
|
||||
this.isExtended = false;
|
||||
this.inProgress = false;
|
||||
this.$content = $('.problem-content-container');
|
||||
this.$contentBound = this.$content.closest('.section');
|
||||
this.$scratchpadContainer = $('.scratchpad-container');
|
||||
}
|
||||
|
||||
async extend() {
|
||||
if (this.inProgress) return;
|
||||
if (this.isExtended) return;
|
||||
this.inProgress = true;
|
||||
|
||||
const bound = this.$contentBound
|
||||
.get(0)
|
||||
.getBoundingClientRect();
|
||||
|
||||
this.$content.transition({ opacity: 0 }, { duration: 100 });
|
||||
await delay(100);
|
||||
|
||||
Navigation.instance.floating.set('scratchpad', true);
|
||||
Navigation.instance.logoVisible.set('scratchpad', true);
|
||||
Navigation.instance.expanded.set('scratchpad', true);
|
||||
$('body').addClass('header--collapsed mode--scratchpad');
|
||||
await this.$scratchpadContainer
|
||||
.css({
|
||||
left: bound.left,
|
||||
top: bound.top,
|
||||
width: bound.width,
|
||||
height: bound.height,
|
||||
})
|
||||
.show()
|
||||
.transition({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}, {
|
||||
duration: 500,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
$('.main > .row').hide();
|
||||
$('.footer').hide();
|
||||
$(window).scrollTop(0);
|
||||
|
||||
this.inProgress = false;
|
||||
this.isExtended = true;
|
||||
}
|
||||
|
||||
async collapse() {
|
||||
if (this.inProgress) return;
|
||||
if (!this.isExtended) return;
|
||||
this.inProgress = true;
|
||||
|
||||
$(window).scrollTop(0);
|
||||
$('.main > .row').show();
|
||||
$('.footer').show();
|
||||
|
||||
const bound = this.$contentBound
|
||||
.get(0)
|
||||
.getBoundingClientRect();
|
||||
|
||||
Navigation.instance.floating.set('scratchpad', false);
|
||||
Navigation.instance.logoVisible.set('scratchpad', false);
|
||||
Navigation.instance.expanded.set('scratchpad', false);
|
||||
|
||||
$('body').removeClass('header--collapsed mode--scratchpad');
|
||||
|
||||
await this.$scratchpadContainer
|
||||
.transition({
|
||||
left: bound.left,
|
||||
top: bound.top,
|
||||
width: bound.width,
|
||||
height: bound.height,
|
||||
}, {
|
||||
duration: 500,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
this.$scratchpadContainer.hide();
|
||||
this.$content.transition({ opacity: 1 }, { duration: 100 });
|
||||
|
||||
this.inProgress = false;
|
||||
this.isExtended = false;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isExtended) this.collapse();
|
||||
else this.extend();
|
||||
}
|
||||
}
|
||||
|
||||
const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homework_detail_problem'], () => {
|
||||
let reactLoaded = false;
|
||||
let renderReact = null;
|
||||
let unmountReact = null;
|
||||
const extender = new ProblemPageExtender();
|
||||
|
||||
const copyProblemDialog = new ActionDialog({
|
||||
$body: $('.dialog__body--send-to > div'),
|
||||
onDispatch(action) {
|
||||
const $target = copyProblemDialog.$dom.find('[name="target"]');
|
||||
if (action === 'ok' && $target.val() === '') {
|
||||
$target.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
copyProblemDialog.clear = function () {
|
||||
this.$dom.find('[name="target"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
async function handleClickCopyProblem() {
|
||||
const action = await copyProblemDialog.clear().open();
|
||||
if (action !== 'ok') return;
|
||||
const target = copyProblemDialog.$dom.find('[name="target"]').val();
|
||||
try {
|
||||
await request.post(UiContext.postCopyUrl, {
|
||||
operation: 'send',
|
||||
target,
|
||||
pids: [UiContext.problemNumId],
|
||||
});
|
||||
Notification.send(i18n('Problem Sended.'));
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function scratchpadFadeIn() {
|
||||
await $('#scratchpad')
|
||||
.transition({
|
||||
opacity: 1,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
|
||||
async function scratchpadFadeOut() {
|
||||
await $('#scratchpad')
|
||||
.transition({
|
||||
opacity: 0,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
|
||||
async function loadReact() {
|
||||
if (reactLoaded) return;
|
||||
$('.loader-container').show();
|
||||
|
||||
const { default: SockJs } = await import('../components/socket');
|
||||
const { default: ScratchpadApp } = await import('../components/scratchpad');
|
||||
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
|
||||
const {
|
||||
React, render, unmountComponentAtNode, Provider, store,
|
||||
} = await loadReactRedux(ScratchpadReducer);
|
||||
|
||||
const sock = new SockJs(UiContext.pretestConnUrl);
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
store.dispatch({
|
||||
type: 'SCRATCHPAD_RECORDS_PUSH',
|
||||
payload: msg,
|
||||
});
|
||||
};
|
||||
|
||||
renderReact = () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ScratchpadApp />
|
||||
</Provider>,
|
||||
$('#scratchpad').get(0),
|
||||
);
|
||||
};
|
||||
unmountReact = () => {
|
||||
unmountComponentAtNode($('#scratchpad').get(0));
|
||||
};
|
||||
reactLoaded = true;
|
||||
$('.loader-container').hide();
|
||||
}
|
||||
|
||||
async function enterScratchpadMode() {
|
||||
await extender.extend();
|
||||
await loadReact();
|
||||
renderReact();
|
||||
await scratchpadFadeIn();
|
||||
}
|
||||
|
||||
async function leaveScratchpadMode() {
|
||||
await scratchpadFadeOut();
|
||||
$('.problem-content-container').append($('.problem-content'));
|
||||
await extender.collapse();
|
||||
unmountReact();
|
||||
}
|
||||
|
||||
$(document).on('click', '[name="problem-sidebar__open-scratchpad"]', (ev) => {
|
||||
enterScratchpadMode();
|
||||
ev.preventDefault();
|
||||
});
|
||||
$(document).on('click', '[name="problem-sidebar__quit-scratchpad"]', (ev) => {
|
||||
leaveScratchpadMode();
|
||||
ev.preventDefault();
|
||||
});
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
$('[name="problem-sidebar__send-to"]').click(() => handleClickCopyProblem());
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,175 @@
|
||||
import _ from 'lodash';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import request from 'vj/utils/request';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||
|
||||
const categories = {};
|
||||
const dirtyCategories = [];
|
||||
const selections = [];
|
||||
const tags = [];
|
||||
|
||||
function setDomSelected($dom, selected) {
|
||||
if (selected) $dom.addClass('selected');
|
||||
else $dom.removeClass('selected');
|
||||
}
|
||||
|
||||
async function updateSelection() {
|
||||
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
||||
let item = categories[category];
|
||||
const isSelected = item.select || _.some(item.children, (c) => c.select);
|
||||
setDomSelected(item.$tag, isSelected);
|
||||
if (isSelected) selections.push(category);
|
||||
else _.pull(selections, category);
|
||||
if (type === 'subcategory') {
|
||||
item = categories[category].children[subcategory];
|
||||
setDomSelected(item.$tag, item.select);
|
||||
const selectionName = subcategory;
|
||||
if (item.select) selections.push(selectionName);
|
||||
else _.pull(selections, selectionName);
|
||||
}
|
||||
});
|
||||
const requestCategoryTags = _.uniq(selections
|
||||
.filter((s) => s.indexOf(',') !== -1)
|
||||
.map((s) => s.split(',')[0]));
|
||||
// drop the category if its subcategory is selected
|
||||
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
|
||||
dirtyCategories.length = 0;
|
||||
const $txt = $('[name="tag"]');
|
||||
$txt.val([...requestTags, ...tags].join(', '));
|
||||
}
|
||||
|
||||
function findCategory(name) {
|
||||
const keys = Object.keys(categories);
|
||||
if (keys.includes(name)) return [name, null];
|
||||
for (const category of keys) {
|
||||
const subkeys = Object.keys(categories[category].children);
|
||||
if (subkeys.includes(name)) return [category, name];
|
||||
}
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
function parseCategorySelection() {
|
||||
const $txt = $('[name="tag"]');
|
||||
tags.length = 0;
|
||||
$txt.val().split(',').map((name) => name.trim()).forEach((name) => {
|
||||
if (!name) return;
|
||||
const [category, subcategory] = findCategory(name);
|
||||
console.log(name, category, subcategory);
|
||||
if (!category) tags.push(name);
|
||||
else if (!subcategory) {
|
||||
categories[category].select = true;
|
||||
dirtyCategories.push({ type: 'category', category });
|
||||
} else if (category) {
|
||||
categories[category].children[subcategory].select = true;
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
}
|
||||
});
|
||||
updateSelection();
|
||||
}
|
||||
|
||||
function buildCategoryFilter() {
|
||||
const $container = $('[data-widget-cf-container]');
|
||||
if (!$container) return;
|
||||
$container.attr('class', 'widget--category-filter row small-up-3 medium-up-2');
|
||||
$container.children('li').get().forEach((category) => {
|
||||
const $category = $(category)
|
||||
.attr('class', 'widget--category-filter__category column');
|
||||
const $categoryTag = $category
|
||||
.find('.section__title a')
|
||||
.remove()
|
||||
.attr('class', 'widget--category-filter__category-tag');
|
||||
const categoryText = $categoryTag.text();
|
||||
const $drop = $category
|
||||
.children('.chip-list')
|
||||
.remove()
|
||||
.attr('class', 'widget--category-filter__drop');
|
||||
const treeItem = {
|
||||
select: false,
|
||||
$tag: $categoryTag,
|
||||
children: {},
|
||||
};
|
||||
categories[categoryText] = treeItem;
|
||||
$category.empty().append($categoryTag);
|
||||
if ($drop.length > 0) {
|
||||
const $subCategoryTags = $drop
|
||||
.children('li')
|
||||
.attr('class', 'widget--category-filter__subcategory')
|
||||
.find('a')
|
||||
.attr('class', 'widget--category-filter__subcategory-tag')
|
||||
.attr('data-category', categoryText);
|
||||
$subCategoryTags.get().forEach((subCategoryTag) => {
|
||||
const $tag = $(subCategoryTag);
|
||||
treeItem.children[$tag.text()] = { select: false, $tag };
|
||||
});
|
||||
Dropdown.getOrConstruct($categoryTag, {
|
||||
target: $drop[0],
|
||||
position: 'left center',
|
||||
});
|
||||
}
|
||||
});
|
||||
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
|
||||
const category = $(ev.currentTarget).text();
|
||||
const treeItem = categories[category];
|
||||
// the effect should be cancelSelect if it is shown as selected when clicking
|
||||
const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select;
|
||||
treeItem.select = shouldSelect;
|
||||
dirtyCategories.push({ type: 'category', category });
|
||||
if (!shouldSelect) {
|
||||
// de-select children
|
||||
_.forEach(treeItem.children, (treeSubItem, subcategory) => {
|
||||
if (treeSubItem.select) {
|
||||
treeSubItem.select = false; // eslint-disable-line no-param-reassign
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
}
|
||||
});
|
||||
}
|
||||
updateSelection();
|
||||
ev.preventDefault();
|
||||
});
|
||||
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
|
||||
const subcategory = $(ev.currentTarget).text();
|
||||
const category = $(ev.currentTarget).attr('data-category');
|
||||
const treeItem = categories[category].children[subcategory];
|
||||
treeItem.select = !treeItem.select;
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
updateSelection();
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
const page = new NamedPage('problem_edit', async () => {
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (confirmed) {
|
||||
return request.post('.', { operation: 'delete' }).then((res) => {
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
const message = 'Confirm deleting this problem? Its files, submissions, discussions and solutions will be deleted as well.';
|
||||
return new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n(message)}</p>
|
||||
</div>`,
|
||||
}).open().then((action) => {
|
||||
if (action !== 'yes') return;
|
||||
confirmed = true;
|
||||
ev.target.click();
|
||||
});
|
||||
});
|
||||
$(document).on('change', '[name="tag"]', parseCategorySelection);
|
||||
buildCategoryFilter();
|
||||
parseCategorySelection();
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,73 @@
|
||||
.page--problem_edit
|
||||
.widget--category-filter
|
||||
&__category
|
||||
margin-bottom: rem(6px)
|
||||
|
||||
&-tag
|
||||
display: inline-block
|
||||
padding: rem(5px 6px)
|
||||
|
||||
&:hover
|
||||
color: #FFF
|
||||
background: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
&.selected
|
||||
color: #FFF
|
||||
background: $secondary-color
|
||||
|
||||
&__drop
|
||||
position: relative
|
||||
font-size: rem($font-size-small)
|
||||
width: rem(170px)
|
||||
padding: rem(10px)
|
||||
margin-right: rem(10px)
|
||||
background: $menu-drop-bg-color
|
||||
box-shadow: $menu-drop-shadow
|
||||
|
||||
&:before
|
||||
content: ''
|
||||
display: block
|
||||
position: absolute
|
||||
width: 0
|
||||
height: 0
|
||||
border: rem($menu-drop-triangle-size) solid transparent
|
||||
border-top-color: #FFF
|
||||
border-left-color: #FFF
|
||||
box-shadow: $menu-drop-triangle-shadow
|
||||
transform: rotate(135deg)
|
||||
transform-origin: 0 0
|
||||
pointer-events: none
|
||||
left: 100%
|
||||
top: 50%
|
||||
margin-left: rem(floor($menu-drop-triangle-size * 1.414))
|
||||
|
||||
&__subcategory
|
||||
margin: rem(2px)
|
||||
display: inline-block
|
||||
|
||||
&-tag
|
||||
display: inline-block
|
||||
padding: rem(5px 6px)
|
||||
|
||||
&:hover
|
||||
color: #FFF
|
||||
background: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
&.selected
|
||||
color: #FFF
|
||||
background: $secondary-color
|
||||
|
||||
.drop-target-attached-right .widget--category-filter__drop
|
||||
margin-right: 0
|
||||
margin-left: rem(10px)
|
||||
|
||||
&:before
|
||||
transform-origin: 100% 0
|
||||
transform: rotate(-45deg)
|
||||
left: auto
|
||||
right: 100%
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
margin-top: rem(floor($menu-drop-triangle-size * -1.414))
|
||||
@@ -0,0 +1,181 @@
|
||||
import { WritableStream } from 'web-streams-polyfill/dist/ponyfill.es6';
|
||||
import * as streamsaver from 'streamsaver';
|
||||
import _ from 'lodash';
|
||||
import { createZipStream } from 'vj/utils/zip';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import request from 'vj/utils/request';
|
||||
import pipeStream from 'vj/utils/pipeStream';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
try {
|
||||
// Firefox have no WritableStream
|
||||
if (!window.WritableStream) streamsaver.WritableStream = WritableStream;
|
||||
streamsaver.mitm = '/streamsaver/mitm.html';
|
||||
} catch (e) {
|
||||
// Extending not allowed in snowpack
|
||||
}
|
||||
|
||||
async function downloadProblemFilesAsArchive(type, files) {
|
||||
const { links, pdoc } = await request.post('', { operation: 'get_links', files, type });
|
||||
const fileStream = streamsaver.createWriteStream(`${pdoc.docId} ${pdoc.title}.zip`);
|
||||
let i = 0;
|
||||
const targets = [];
|
||||
for (const filename of Object.keys(links)) targets.push({ filename, downloadUrl: links[filename] });
|
||||
const zipStream = createZipStream({
|
||||
// eslint-disable-next-line consistent-return
|
||||
async pull(ctrl) {
|
||||
if (i === targets.length) return ctrl.close();
|
||||
try {
|
||||
const response = await fetch(targets[i].downloadUrl);
|
||||
if (!response.ok) throw response.statusText;
|
||||
ctrl.enqueue({
|
||||
name: targets[i].filename,
|
||||
stream: () => response.body,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
stopDownload();
|
||||
Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()]));
|
||||
}
|
||||
i++;
|
||||
},
|
||||
});
|
||||
const abortCallbackReceiver = {};
|
||||
function stopDownload() { abortCallbackReceiver.abort(); }
|
||||
let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext;
|
||||
function onBeforeUnload(e) {
|
||||
if (isBeforeUnloadTriggeredByLibrary) {
|
||||
isBeforeUnloadTriggeredByLibrary = false;
|
||||
return;
|
||||
}
|
||||
e.returnValue = '';
|
||||
}
|
||||
window.addEventListener('unload', stopDownload);
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
await pipeStream(zipStream, fileStream, abortCallbackReceiver);
|
||||
window.removeEventListener('unload', stopDownload);
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
}
|
||||
|
||||
const page = new NamedPage('problem_files', () => {
|
||||
function ensureAndGetSelectedFiles(type) {
|
||||
const files = _.map(
|
||||
$(`.problem-files-${type} tbody [data-checkbox-group="${type}"]:checked`),
|
||||
(ch) => $(ch).closest('tr').attr('data-filename'),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
Notification.error(i18n('Please select at least one file to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function handleClickUpload(type, files) {
|
||||
if (!files) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
await new Promise((resolve) => { input.onchange = resolve; });
|
||||
files = input.files;
|
||||
}
|
||||
try {
|
||||
Notification.info('Uploading files...');
|
||||
const tasks = [];
|
||||
for (const file of files) {
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('file', file);
|
||||
data.append('type', type);
|
||||
data.append('operation', 'upload_file');
|
||||
tasks.push(request.postFile('', data));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
Notification.success('File uploaded successfully.');
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(`File upload fail: ${e.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickDownloadSelected(type) {
|
||||
const selectedFiles = ensureAndGetSelectedFiles(type);
|
||||
if (selectedFiles === null) return;
|
||||
await downloadProblemFilesAsArchive(type, selectedFiles);
|
||||
}
|
||||
|
||||
async function handleClickRemoveSelected(type) {
|
||||
const selectedFiles = ensureAndGetSelectedFiles(type);
|
||||
if (selectedFiles === null) return;
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm to delete the selected files?')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'delete_files',
|
||||
files: selectedFiles,
|
||||
type,
|
||||
});
|
||||
Notification.success(i18n('Selected files have been deleted.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDragOver(type, ev) {
|
||||
ev.preventDefault();
|
||||
// TODO display a drag-drop allowed hint
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDrop(type, ev) {
|
||||
ev.preventDefault();
|
||||
ev = ev.originalEvent;
|
||||
const files = [];
|
||||
if (ev.dataTransfer.items) {
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const file = ev.dataTransfer.items[i].getAsFile();
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
files.push(ev.dataTransfer.files[i]);
|
||||
}
|
||||
}
|
||||
handleClickUpload(type, files);
|
||||
}
|
||||
|
||||
$('.problem-files-testdata').on('dragover', (ev) => handleDragOver('testdata', ev));
|
||||
$('.problem-files-additional_file').on('dragover', (ev) => handleDragOver('additional_file', ev));
|
||||
$('.problem-files-testdata').on('drop', (ev) => handleDrop('testdata', ev));
|
||||
$('.problem-files-additional_file').on('drop', (ev) => handleDrop('additional_file', ev));
|
||||
$('[name="upload_testdata"]').on('click', () => handleClickUpload('testdata'));
|
||||
$('[name="download_selected_testdata"]').on('click', () => handleClickDownloadSelected('testdata'));
|
||||
$('[name="remove_selected_testdata"]').on('click', () => handleClickRemoveSelected('testdata'));
|
||||
$('[name="upload_file"]').on('click', () => handleClickUpload('additional_file'));
|
||||
$('[name="download_selected_file"]').on('click', () => handleClickDownloadSelected('additional_file'));
|
||||
$('[name="remove_selected_file"]').on('click', () => handleClickRemoveSelected('additional_file'));
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,6 @@
|
||||
.page--problem_files
|
||||
.col--checkbox
|
||||
width: 60px
|
||||
|
||||
.col--size
|
||||
width: 120px
|
||||
@@ -0,0 +1,6 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('problem_import', async () => {
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,168 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||
import pjax from 'vj/utils/pjax';
|
||||
import substitute from 'vj/utils/substitute';
|
||||
|
||||
const categories = {};
|
||||
const dirtyCategories = [];
|
||||
const selections = [];
|
||||
|
||||
function setDomSelected($dom, selected) {
|
||||
if (selected) {
|
||||
$dom.addClass('selected');
|
||||
} else {
|
||||
$dom.removeClass('selected');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSelection(sendRequest = true) {
|
||||
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
||||
let item = categories[category];
|
||||
const isSelected = item.select || _.some(item.children, (c) => c.select);
|
||||
setDomSelected(item.$tag, isSelected);
|
||||
if (isSelected) {
|
||||
selections.push(category);
|
||||
} else {
|
||||
_.pull(selections, category);
|
||||
}
|
||||
if (type === 'subcategory') {
|
||||
item = categories[category].children[subcategory];
|
||||
setDomSelected(item.$tag, item.select);
|
||||
const selectionName = subcategory;
|
||||
if (item.select) {
|
||||
selections.push(selectionName);
|
||||
} else {
|
||||
_.pull(selections, selectionName);
|
||||
}
|
||||
}
|
||||
});
|
||||
dirtyCategories.length = 0;
|
||||
if (sendRequest) {
|
||||
// a list of categories which subcategory is selected
|
||||
const requestCategoryTags = _.uniq(selections
|
||||
.filter((s) => s.indexOf(',') !== -1)
|
||||
.map((s) => s.split(',')[0]));
|
||||
// drop the category if its subcategory is selected
|
||||
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
|
||||
let url;
|
||||
if (requestTags.length === 0) {
|
||||
url = UiContext.getProblemUrlWithoutCategory;
|
||||
} else {
|
||||
url = substitute(decodeURIComponent(UiContext.getProblemUrlWithCategory), {
|
||||
category: requestTags
|
||||
.map((tag) => tag.split(',').map(encodeURIComponent).join(','))
|
||||
.join('+'), // build a beautiful URL
|
||||
});
|
||||
}
|
||||
pjax.request({ url });
|
||||
}
|
||||
}
|
||||
|
||||
function buildCategoryFilter() {
|
||||
const $container = $('[data-widget-cf-container]');
|
||||
if (!$container) return;
|
||||
$container.attr('class', 'widget--category-filter row small-up-3 medium-up-2');
|
||||
$container.children('li').get().forEach((category) => {
|
||||
const $category = $(category)
|
||||
.attr('class', 'widget--category-filter__category column');
|
||||
const $categoryTag = $category
|
||||
.find('.section__title a')
|
||||
.remove()
|
||||
.attr('class', 'widget--category-filter__category-tag');
|
||||
const categoryText = $categoryTag.text();
|
||||
const $drop = $category
|
||||
.children('.chip-list')
|
||||
.remove()
|
||||
.attr('class', 'widget--category-filter__drop');
|
||||
const treeItem = {
|
||||
select: false,
|
||||
$tag: $categoryTag,
|
||||
children: {},
|
||||
};
|
||||
categories[categoryText] = treeItem;
|
||||
$category.empty().append($categoryTag);
|
||||
if ($drop.length > 0) {
|
||||
$categoryTag.text(`${$categoryTag.text()}`);
|
||||
const $subCategoryTags = $drop
|
||||
.children('li')
|
||||
.attr('class', 'widget--category-filter__subcategory')
|
||||
.find('a')
|
||||
.attr('class', 'widget--category-filter__subcategory-tag')
|
||||
.attr('data-category', categoryText);
|
||||
$subCategoryTags.get().forEach((subCategoryTag) => {
|
||||
const $tag = $(subCategoryTag);
|
||||
treeItem.children[$tag.text()] = {
|
||||
select: false,
|
||||
$tag,
|
||||
};
|
||||
});
|
||||
Dropdown.getOrConstruct($categoryTag, {
|
||||
target: $drop[0],
|
||||
position: 'left center',
|
||||
});
|
||||
}
|
||||
});
|
||||
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const category = $(ev.currentTarget).text();
|
||||
const treeItem = categories[category];
|
||||
// the effect should be cancelSelect if it is shown as selected when clicking
|
||||
const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select;
|
||||
treeItem.select = shouldSelect;
|
||||
dirtyCategories.push({ type: 'category', category });
|
||||
if (!shouldSelect) {
|
||||
// de-select children
|
||||
_.forEach(treeItem.children, (treeSubItem, subcategory) => {
|
||||
if (treeSubItem.select) {
|
||||
treeSubItem.select = false; // eslint-disable-line no-param-reassign
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
}
|
||||
});
|
||||
}
|
||||
updateSelection();
|
||||
ev.preventDefault();
|
||||
});
|
||||
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const subcategory = $(ev.currentTarget).text();
|
||||
const category = $(ev.currentTarget).attr('data-category');
|
||||
const treeItem = categories[category].children[subcategory];
|
||||
treeItem.select = !treeItem.select;
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
updateSelection();
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function parseCategorySelection() {
|
||||
UiContext.currentCategory.split(' ').forEach((cline) => {
|
||||
const [category, subcategory] = cline.split(',');
|
||||
if (!categories[category]) {
|
||||
return;
|
||||
}
|
||||
if (subcategory && !categories[category].children[subcategory]) {
|
||||
return;
|
||||
}
|
||||
if (!subcategory) {
|
||||
categories[category].select = true;
|
||||
dirtyCategories.push({ type: 'category', category });
|
||||
} else {
|
||||
categories[category].children[subcategory].select = true;
|
||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
||||
}
|
||||
});
|
||||
updateSelection(false);
|
||||
}
|
||||
|
||||
const page = new NamedPage(['problem_main', 'problem_category'], () => {
|
||||
buildCategoryFilter();
|
||||
parseCategorySelection();
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,99 @@
|
||||
.page--problem_main
|
||||
.page--problem_category
|
||||
.page--problem_settings
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
position: relative
|
||||
|
||||
.col--submit_n, .col--ac_rate, .col--difficulty
|
||||
width: rem(70px)
|
||||
text-align: center
|
||||
|
||||
.col--status
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--submit_n, .col--difficulty
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--status,
|
||||
.col--submit_n,
|
||||
.col--difficulty
|
||||
display: none
|
||||
|
||||
.col--ac_rate
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
|
||||
.widget--category-filter
|
||||
&__category
|
||||
margin-bottom: rem(6px)
|
||||
|
||||
&-tag
|
||||
display: inline-block
|
||||
padding: rem(5px 6px)
|
||||
|
||||
&:hover
|
||||
color: #FFF
|
||||
background: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
&.selected
|
||||
color: #FFF
|
||||
background: $secondary-color
|
||||
|
||||
&__drop
|
||||
position: relative
|
||||
font-size: rem($font-size-small)
|
||||
width: rem(170px)
|
||||
padding: rem(10px)
|
||||
margin-right: rem(10px)
|
||||
background: $menu-drop-bg-color
|
||||
box-shadow: $menu-drop-shadow
|
||||
|
||||
&:before
|
||||
content: ''
|
||||
display: block
|
||||
position: absolute
|
||||
width: 0
|
||||
height: 0
|
||||
border: rem($menu-drop-triangle-size) solid transparent
|
||||
border-top-color: #FFF
|
||||
border-left-color: #FFF
|
||||
box-shadow: $menu-drop-triangle-shadow
|
||||
transform: rotate(135deg)
|
||||
transform-origin: 0 0
|
||||
pointer-events: none
|
||||
left: 100%
|
||||
top: 50%
|
||||
margin-left: rem(floor($menu-drop-triangle-size * 1.414))
|
||||
|
||||
&__subcategory
|
||||
margin: rem(2px)
|
||||
display: inline-block
|
||||
|
||||
&-tag
|
||||
display: inline-block
|
||||
padding: rem(5px 6px)
|
||||
|
||||
&:hover
|
||||
color: #FFF
|
||||
background: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
&.selected
|
||||
color: #FFF
|
||||
background: $secondary-color
|
||||
|
||||
.drop-target-attached-right .widget--category-filter__drop
|
||||
margin-right: 0
|
||||
margin-left: rem(10px)
|
||||
|
||||
&:before
|
||||
transform-origin: 100% 0
|
||||
transform: rotate(-45deg)
|
||||
left: auto
|
||||
right: 100%
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
margin-top: rem(floor($menu-drop-triangle-size * -1.414))
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('problem_solution', async () => {
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('problem_submit', async () => {
|
||||
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
|
||||
$(ev.currentTarget).hide();
|
||||
$('[name="problem-sidebar__categories"]').show();
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,12 @@
|
||||
.page--problem_submit
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
|
||||
.col--submit-at
|
||||
width: rem(160px)
|
||||
text-align: center
|
||||
|
||||
+mobile()
|
||||
.col--memory,
|
||||
.col--time
|
||||
display: none
|
||||
@@ -0,0 +1,16 @@
|
||||
.page--ranking
|
||||
.col--rank
|
||||
width: rem(70px)
|
||||
position: relative
|
||||
|
||||
.col--user
|
||||
width: rem(200px)
|
||||
text-align: center
|
||||
|
||||
.col--rp
|
||||
width: rem(80px)
|
||||
text-align: right
|
||||
|
||||
+mobile()
|
||||
.col--bio
|
||||
display: none
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('record_detail', async () => {
|
||||
const { default: SockJs } = await import('../components/socket');
|
||||
const { DiffDOM } = await import('diff-dom');
|
||||
|
||||
const sock = new SockJs(UiContext.socketUrl);
|
||||
const dd = new DiffDOM();
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
const newStatus = $(msg.status_html);
|
||||
const oldStatus = $('#status');
|
||||
dd.apply(oldStatus[0], dd.diff(oldStatus[0], newStatus[0]));
|
||||
const newSummary = $(msg.summary_html);
|
||||
const oldSummary = $('#summary');
|
||||
dd.apply(oldSummary[0], dd.diff(oldSummary[0], newSummary[0]));
|
||||
};
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,26 @@
|
||||
.page--record_detail
|
||||
|
||||
.compiler-text
|
||||
padding: rem(20px 0)
|
||||
font-size: rem($font-size-small)
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
.col--case
|
||||
width: rem(70px)
|
||||
|
||||
.col--memory, .col--time
|
||||
width: rem(120px)
|
||||
|
||||
.col--case, .col--status
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--case
|
||||
display: none
|
||||
|
||||
.col--time
|
||||
width: rem(70px)
|
||||
|
||||
.col--memory
|
||||
width: rem(100px)
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
|
||||
const page = new NamedPage('record_main', async () => {
|
||||
const { default: SockJs } = await import('../components/socket');
|
||||
const { DiffDOM } = await import('diff-dom');
|
||||
|
||||
const sock = new SockJs(UiContext.socketUrl);
|
||||
const dd = new DiffDOM();
|
||||
|
||||
sock.onopen = () => {
|
||||
sock.send(JSON.stringify({ rids: UiContext.rids }));
|
||||
};
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
const $newTr = $(msg.html);
|
||||
const $oldTr = $(`.record_main__table tr[data-rid="${$newTr.attr('data-rid')}"]`);
|
||||
if ($oldTr.length) {
|
||||
$oldTr.trigger('vjContentRemove');
|
||||
dd.apply($oldTr[0], dd.diff($oldTr[0], $newTr[0]));
|
||||
$oldTr.trigger('vjContentNew');
|
||||
} else {
|
||||
$('.record_main__table tbody').prepend($newTr);
|
||||
$newTr.trigger('vjContentNew');
|
||||
}
|
||||
};
|
||||
UserSelectAutoComplete.getOrConstruct($('.filter-user [name="uidOrName"]'), {
|
||||
clearDefaultValue: false,
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,55 @@
|
||||
.page--record_main
|
||||
.col--status
|
||||
width: rem(170px)
|
||||
position: relative
|
||||
|
||||
.col--submit-at
|
||||
width: rem(120px)
|
||||
text-align: center
|
||||
|
||||
&.nojs .col--submit-at
|
||||
width: rem(160px)
|
||||
|
||||
.col--submit-by
|
||||
width: rem(120px)
|
||||
text-align: right
|
||||
|
||||
.col--time, .col--memory, .col--lang
|
||||
width: rem(100px)
|
||||
text-align: center
|
||||
|
||||
.col--status, .col--submit-by, .col--lang
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--status__text
|
||||
position: relative
|
||||
z-index: 2
|
||||
|
||||
.col--status__progress-container
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
z-index: 1
|
||||
|
||||
.col--status__progress
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
height: 100%
|
||||
background: #fff6d1
|
||||
|
||||
+tablet()
|
||||
.col--time,
|
||||
.col--memory,
|
||||
.col--lang
|
||||
display: none
|
||||
|
||||
+mobile()
|
||||
.col--submit-by,
|
||||
.col--submit-at,
|
||||
.col--time,
|
||||
.col--memory,
|
||||
.col--lang
|
||||
display: none
|
||||
@@ -0,0 +1,17 @@
|
||||
.page--status
|
||||
.col--id
|
||||
width: 70px
|
||||
|
||||
.col--status
|
||||
width: 100px
|
||||
|
||||
.col--memory
|
||||
width: 150px
|
||||
|
||||
.col--request
|
||||
width: 100px
|
||||
|
||||
+mobile()
|
||||
.col--os
|
||||
.col--cpu
|
||||
display: none
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||
|
||||
async function handleSectionExpand(ev) {
|
||||
const $section = $(ev.currentTarget).closest('.training__section');
|
||||
if ($section.is('.expanded, .animating')) {
|
||||
return;
|
||||
}
|
||||
$section.addClass('animating');
|
||||
const $detail = $section.find('.training__section__detail');
|
||||
await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 });
|
||||
$section.addClass('expanded');
|
||||
$section.removeClass('collapsed');
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
async function handleSectionCollapse(ev) {
|
||||
const $section = $(ev.currentTarget).closest('.training__section');
|
||||
if ($section.is('.collapsed, .animating')) {
|
||||
return;
|
||||
}
|
||||
$section.addClass('animating');
|
||||
const $detail = $section.find('.training__section__detail');
|
||||
await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 });
|
||||
$section.addClass('collapsed');
|
||||
$section.removeClass('expanded');
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
const page = new NamedPage('training_detail', async () => {
|
||||
$(document).on('click', '[name="training__section__expand"]', handleSectionExpand);
|
||||
$(document).on('click', '[name="training__section__collapse"]', handleSectionCollapse);
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,35 @@
|
||||
.page--training_detail
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
position: relative
|
||||
|
||||
.col--submit_n, .col--ac_rate, .col--difficulty
|
||||
width: rem(70px)
|
||||
text-align: center
|
||||
|
||||
.col--status
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--submit_n, .col--difficulty
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--status,
|
||||
.col--submit_n,
|
||||
.col--difficulty
|
||||
display: none
|
||||
|
||||
.col--ac_rate
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
.training__section
|
||||
&.invalid
|
||||
.training__problems
|
||||
opacity: 0.6
|
||||
|
||||
&.collapsed .collapsed--hidden,
|
||||
&.expanded .expanded--hidden
|
||||
display: none
|
||||
|
||||
&.collapsed .training__section__detail
|
||||
display: none
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import request from 'vj/utils/request';
|
||||
|
||||
export default new NamedPage('training_edit', () => {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (confirmed) {
|
||||
return request.post('.', { operation: 'delete' }).then((res) => {
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
const message = 'Confirm deleting this training? Its status will be deleted as well.';
|
||||
return new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n(message)}</p>
|
||||
</div>`,
|
||||
}).open().then((action) => {
|
||||
if (action !== 'yes') return;
|
||||
confirmed = true;
|
||||
ev.target.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
.page--training_main
|
||||
.training__title
|
||||
line-height: 1.2
|
||||
margin-bottom: 0.4em
|
||||
font-size: rem($font-size-title)
|
||||
|
||||
.training__intro
|
||||
color: lighten($text-1-color, 20%)
|
||||
font-size: rem($font-size-secondary)
|
||||
margin: 1em 0
|
||||
|
||||
.training__item
|
||||
padding-left: rem(5px)
|
||||
|
||||
.training__participants
|
||||
width: rem(80px)
|
||||
|
||||
.training__progress
|
||||
line-height: rem(17px)
|
||||
vertical-align: top
|
||||
|
||||
.training__progress-bar
|
||||
display: inline-block
|
||||
width: rem(100px)
|
||||
height: rem(5px)
|
||||
position: relative
|
||||
background: #EEE
|
||||
margin: rem(6px 0)
|
||||
line-height: 1
|
||||
vertical-align: top
|
||||
|
||||
.training__progress-track
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
height: 100%
|
||||
width: 0%
|
||||
background-color: green
|
||||
|
||||
.secondary.training__list
|
||||
.training__item
|
||||
margin-bottom: rem(15px)
|
||||
.training__title
|
||||
font-size: rem($font-size)
|
||||
|
||||
&.my
|
||||
.training-status--icon
|
||||
font-size: rem($font-size-icon * 1.2)
|
||||
@@ -0,0 +1,23 @@
|
||||
import base64 from 'base-64';
|
||||
import Clipboard from 'clipboard';
|
||||
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import substitute from 'vj/utils/substitute';
|
||||
import Notification from 'vj/components/notification';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
const page = new NamedPage('user_detail', async () => {
|
||||
$('[name="profile_contact_copy"]').get().forEach((el) => {
|
||||
const data = $(el).attr('data-content');
|
||||
const decoded = base64.decode(data);
|
||||
const clip = new Clipboard(el, { text: () => decoded });
|
||||
clip.on('success', () => {
|
||||
Notification.success(substitute(i18n('"{data}" copied to clipboard!'), { data: decoded }), 2000);
|
||||
});
|
||||
clip.on('error', () => {
|
||||
Notification.error(substitute(i18n('Copy "{data}" failed :('), { data: decoded }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,90 @@
|
||||
@import '~vj/components/tab/var.inc.styl'
|
||||
|
||||
.page--user_detail
|
||||
|
||||
.profile-header
|
||||
position: relative
|
||||
background-position: center center
|
||||
background-size: cover
|
||||
min-height: rem(270px)
|
||||
padding-bottom: rem($tab-item-height + $tab-item-shadow-size + 20px)
|
||||
|
||||
+mobile()
|
||||
padding-bottom: rem($tab-item-height-mobile)
|
||||
|
||||
.profile-header__content
|
||||
position: relative
|
||||
padding: rem($section-gap-v) rem($section-gap-h) 0
|
||||
|
||||
.media__left
|
||||
padding-right: rem(30px)
|
||||
|
||||
+mobile()
|
||||
.media, .media__left, .media__body
|
||||
display: block
|
||||
|
||||
.media__left
|
||||
text-align: center
|
||||
|
||||
img
|
||||
display: inline-block
|
||||
|
||||
.media__body
|
||||
margin-top: rem(20px)
|
||||
|
||||
.profile-header__contact-bar
|
||||
position: absolute
|
||||
right: rem($section-gap-h + 5px)
|
||||
top: rem($section-gap-v + 20px)
|
||||
color: #FFF
|
||||
text-align: right
|
||||
|
||||
+below(rupture.hd-cutoff)
|
||||
position: relative
|
||||
right: 0
|
||||
top: 0
|
||||
text-align: left
|
||||
margin: rem(15px 0)
|
||||
|
||||
.profile-header__contact-item
|
||||
vertical-align: top
|
||||
color: rgba(#FFF, 0.8)
|
||||
margin-left: rem(10px)
|
||||
|
||||
+below(rupture.hd-cutoff)
|
||||
margin-left: 0
|
||||
margin-right: rem(15px)
|
||||
|
||||
&:hover
|
||||
color: rgba(#FFF, 1)
|
||||
text-decoration: none
|
||||
|
||||
&.badge--mod
|
||||
background-color: rgba(#FFF, 0.8) !important
|
||||
color: rgba(#000, 0.8) !important
|
||||
|
||||
.profile-header__main
|
||||
h1
|
||||
line-height: 40px
|
||||
color: #FFF
|
||||
|
||||
p
|
||||
line-height: 30px
|
||||
color: #FFF
|
||||
|
||||
small
|
||||
color: rgba(#FFF, 0.8)
|
||||
|
||||
.profile-content
|
||||
position: relative
|
||||
z-index: 2
|
||||
margin-top: rem(-($tab-item-height + $tab-item-shadow-size))
|
||||
|
||||
+mobile()
|
||||
margin-top: rem(-($tab-item-height-mobile))
|
||||
|
||||
.section__tabs
|
||||
padding-top: rem($tab-item-height + $tab-item-shadow-size)
|
||||
|
||||
+mobile()
|
||||
padding-top: rem($tab-item-height-mobile)
|
||||