11 | }
12 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins.sass:
--------------------------------------------------------------------------------
1 | @import 'mixins/maths'
2 |
3 | @import "mixins/color"
4 | @import "mixins/hover"
5 | @import "mixins/ease"
6 | @import "mixins/icon"
7 | @import "mixins/nano"
8 | @import "mixins/other"
9 | @import 'mixins/padding'
10 | @import 'mixins/fontsize'
11 | @import "mixins/position"
12 | @import "mixins/shadow"
13 | @import "mixins/size"
14 | @import "mixins/text"
15 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_icon.sass:
--------------------------------------------------------------------------------
1 | =center-icon
2 | display: table-cell
3 | vertical-align: middle
4 | text-align: center
5 |
6 | =pseudo-circle($size, $color)
7 | position: relative
8 | &::after
9 | content: ''
10 | z-index: -1 // to not mess with icons
11 | +circle($size)
12 | position: absolute
13 | background-color: $color
14 |
--------------------------------------------------------------------------------
/app/components/interaction/Spinners/SquareSpinner/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import './Spinner.styles'
2 | import React from 'react';
3 | import classes from 'classnames';
4 |
5 | export default ({ isSpinning=true, className}) => {
6 | const spinnerClasses = classes("Spinner", className, {
7 | "Spinner-active": isSpinning
8 | })
9 |
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/app/redux/selectors/thread.js:
--------------------------------------------------------------------------------
1 | export const getThreadPosts = (state) => state.thread.posts;
2 | export const getThreadReceivedAt = (state) => state.thread.receivedAt;
3 |
4 | // export const getWatchMetadata = (state) => state.watch.threads;
5 |
6 | export const isThreadBeingWatched = (state, threadID) =>
7 | !!getMonitoredThreads(state)
8 | .find(thread => thread.threadID === threadID)
9 |
--------------------------------------------------------------------------------
/app/components/interaction/ActionButton/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ActionButton
4 | +box-shadow(3)
5 | font-family: $font-main-light
6 | background: transparent
7 | outline: none
8 | border: $primary solid 1px
9 | border-radius: 8px
10 | color: var(--textPrimary)
11 | line-height: 1
12 | position: relative
13 | +clickable
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardSpinner/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './styles';
3 | import {CircleSpinner as Spinner} from '~/components'
4 |
5 | const BoardSpinner = () =>
6 |
7 |
8 |
9 |
10 | BoardSpinner.displayName = 'BoardSpinner';
11 |
12 | export default BoardSpinner;
13 |
--------------------------------------------------------------------------------
/app/components/form/index.js:
--------------------------------------------------------------------------------
1 | export {default as Checkbox} from './Checkbox'
2 | export {default as FileInput} from './FileInput'
3 | export {default as RadioGroup} from './RadioGroup'
4 | export {default as SearchBar} from './SearchBar'
5 | export {default as SearchBarWithIcons} from './SearchBarWithIcons'
6 | export {default as TextArea} from './TextArea'
7 | export {default as TextField} from './TextField'
8 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Title.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { setHTML } from '~/utils/react'
3 |
4 | const Title = ({ title }) => {
5 | return title ? (
6 |
7 |
8 |
9 |
10 | ) : null
11 | }
12 |
13 | Title.displayName = 'Title'
14 |
15 | export default Title
16 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderGroup/HeaderGroup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import cx from 'classnames'
3 |
4 | import './HeaderGroup.styles'
5 | import { Tooltip } from '~/components'
6 |
7 | export default ({children, className, ...restProps}) => {
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderItem/HeaderItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import cx from 'classnames'
3 |
4 | import './HeaderItem.styles'
5 | import Tooltip from '~/components/Tooltip'
6 |
7 | export default ({children, className, ...restProps}) => {
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_fontsize.sass:
--------------------------------------------------------------------------------
1 | $font-ratio: $golden-ratio // 1.618...
2 | $font-ratio: 1.5 // perfect fifth
3 | $font-ratio: 1.25 // major third
4 | $font-ratio: 1.125 // major second
5 | $font-ratio: 1.2 // minor third
6 |
7 |
8 | @mixin font-size($size: 0, $unit: 1rem)
9 | font-size: font-size($size, $unit)
10 |
11 | @function font-size($size, $unit)
12 | @return pow($font-ratio, $size) * $unit
13 |
--------------------------------------------------------------------------------
/app/components/__old__/Hierarchy/Hierarchy.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Hierarchy
4 | display: inline-block
5 | padding: 0 10px
6 | font-size: $font-size-large
7 |
8 | li
9 | display: inline-block
10 |
11 | span
12 | margin: 0 2px
13 | color: var(--textSecondary)
14 | font-family: Consolas
15 |
16 | .threadID
17 | font-family: for-for(Hierarchy__threadID)
18 |
--------------------------------------------------------------------------------
/app/components/form/RadioGroup/Radio.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | const Radio = ({ isActive }) => {
5 | return (
6 |
11 | );
12 | };
13 |
14 | Radio.displayName = 'Radio';
15 |
16 | export default Radio;
17 |
--------------------------------------------------------------------------------
/app/views/CinemaView.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | import Cinema from '~/modules/Cinema';
5 |
6 | const CinemaView = ({ className }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | CinemaView.displayName = 'CinemaView';
15 |
16 | export default CinemaView;
17 |
--------------------------------------------------------------------------------
/app/components/__old__/App/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import store from '~/redux/store';
4 |
5 | import Views from '~/views';
6 | import Services from '~/services'
7 |
8 | const App = () => (
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardMetadata/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .BoardMetadata
4 | width: 100vw
5 | margin: 0 auto
6 | padding: 0 60px
7 | +padding-top(2)
8 | display: flex
9 | justify-content: space-between
10 | font-size: $subheader-font-size
11 | color: var(--textSecondary)
12 |
13 | .highlight
14 | color: $primary-light
15 |
16 | .stat
17 | margin-left: 8px
18 |
--------------------------------------------------------------------------------
/app/modules/Board/components/TitledIcon/TitledIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './TitledIcon.styles';
4 |
5 | import {Icon} from '~/components';
6 |
7 | export default function TitledIcon ({ name, title, className }) {
8 | return
9 |
10 | {title}
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/modules/Header/components/TitledIcon/TitledIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './TitledIcon.styles';
4 |
5 | import {Icon} from '~/components';
6 |
7 | export default function TitledIcon ({ name, title, className }) {
8 | return
9 |
10 | {title}
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/modules/Panels/ArchivePanel/ArchivePanel.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import classes from 'classnames'
3 |
4 | import Panel from '../../components/Panel'
5 |
6 |
7 | export default function ArchivePanel({isActive}) {
8 | return
9 | Archive Panel
10 |
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/app/redux/reducers/apiReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 | import initialState from '../initialState';
3 | import { createReducer, mergeState } from '~/utils/redux';
4 |
5 | export default createReducer(initialState.api, {
6 | [types.BOARD_LOADED]: (state, action) =>
7 | mergeState(state, {
8 | board: {
9 | [action.receivedAt]: action.posts
10 | }
11 | }),
12 | });
13 |
--------------------------------------------------------------------------------
/config/webpack/ConsoleClearPlugin.js:
--------------------------------------------------------------------------------
1 | const clear = require('clear');
2 | const clc = require('cli-color');
3 |
4 | function ConsoleClearPlugin() {};
5 | ConsoleClearPlugin.prototype.apply = function(compiler) {
6 | compiler.plugin("compile", (params) => {
7 | clear();
8 | console.log(clc.whiteBright("\nLurka is compiling..."));
9 | });
10 | };
11 |
12 | module.exports = module.exports.default = ConsoleClearPlugin;
13 |
--------------------------------------------------------------------------------
/app/modules/Board/tests/index.suite.js:
--------------------------------------------------------------------------------
1 | // import integrationTests from '..';
2 | import React from 'react';
3 | import Board from '..';
4 | import configureStore from 'redux-mock-store'
5 |
6 | const middlewares = []
7 | const mockStore = configureStore(middlewares)
8 |
9 | export default createSuite("Board", () => {
10 | // it("mounts", () => {
11 | // shallow(
);
12 | // });
13 |
14 | // integrationTests();
15 |
16 | });
17 |
--------------------------------------------------------------------------------
/app/redux/reducers/settingsReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 | import initialState from '../initialState';
3 | import { createReducer, mergeState } from '~/utils/redux';
4 |
5 | export default createReducer(initialState.settings, {
6 | [types.SETTING_CHANGED]: (state, action) =>
7 | mergeState(state, mergeState({}, state.external, {
8 | [action.payload.setting]: action.payload.value
9 | }))
10 | });
11 |
--------------------------------------------------------------------------------
/app/components/form/Checkbox/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './styles'
3 |
4 | export default ({onChange, isChecked}) => {
5 | const uid = "p"+Date.now()
6 | return (
7 |
8 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/app/views/DashboardView.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames'
3 |
4 | import Dashboard from '~/modules/Dashboard';
5 |
6 | const DashboardView = ({ className }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | DashboardView.displayName = 'DashboardView';
15 |
16 | export default DashboardView;
17 |
--------------------------------------------------------------------------------
/app/modules/Header/components/VerticallyTitledIcon/VerticallyTitledIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './VerticallyTitledIcon.styles';
4 |
5 | import {Icon} from '~/components';
6 |
7 | export default function VerticallyTitledIcon ({ title, children, className }) {
8 | return
9 | {children}
10 | {title}
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/redux/actions/board/searchBoard.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import { getBoardPosts } from '~/redux/selectors/board';
3 |
4 | export default function searchBoard (searchTerm) {
5 | console.log("Action searchBoard()");
6 |
7 | return dispatch =>
8 | dispatch(boardSearched(searchTerm));
9 | }
10 |
11 | export const boardSearched = (searchTerm) => ({
12 | type: types.BOARD_SEARCHED,
13 | payload: searchTerm
14 | })
15 |
--------------------------------------------------------------------------------
/app/components/interaction/Spinners/SquareSpinner/Spinner.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Spinner
4 | +center(80px, 80px)
5 | background-color: var(--primary)
6 | position: fixed
7 | animation: rotatespinner 1.2s infinite ease-in-out
8 | z-index: 10
9 | display: none
10 | animation-play-state: paused
11 |
12 | &.Spinner-active
13 | display: initial !important
14 | animation-play-state: running !important
15 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/components/HomeBoard/HomeBoard.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .HomeBoard
4 | +square(170px)
5 | position: absolute
6 | bottom: 0
7 | right: 100px
8 | background-color: #FFF
9 | font-size: 40px
10 |
11 | &__container
12 | display: table-cell
13 | vertical-align: center
14 |
15 | &__boardName
16 | color: var(--primary)
17 | &__icon
18 | color: var(--primary)
19 |
20 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_nano.sass:
--------------------------------------------------------------------------------
1 | =nano-can-expand($width)
2 | cursor: grab
3 | transition: width .24s .2s ease-in
4 |
5 | &:hover, &:active
6 | width: $width
7 | transition: width .24s ease(google, out)
8 | box-shadow: inset 2px 0 2px $grey-dark
9 |
10 | .nano-slider
11 | width: 50%
12 | margin: 0 auto
13 |
14 | =nano-rect($width:4px)
15 | width: $width
16 | border-radius: 0 !important
17 |
--------------------------------------------------------------------------------
/app/components/elements/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './Button.styles';
4 |
5 | const Button = ({ className, children, borderAccent, ...restProps }) => {
6 | return
9 | {children}
10 |
11 | };
12 |
13 | Button.displayName = 'Button';
14 |
15 | export default Button;
16 |
--------------------------------------------------------------------------------
/app/components/elements/ButtonCircle/Circle/Circle.jsx:
--------------------------------------------------------------------------------
1 | import './Circle.styles'
2 | import React, { PropTypes } from 'react';
3 |
4 | const Circle = ({ className, children }) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | Circle.displayName = 'Circle';
13 |
14 | Circle.propTypes = {
15 | className: PropTypes.string,
16 | };
17 |
18 | export default Circle;
19 |
--------------------------------------------------------------------------------
/app/components/interaction/HoverUnderline/index.jsx:
--------------------------------------------------------------------------------
1 | import {cloneElement, Children} from 'react';
2 | import cx from 'classnames';
3 |
4 | import './styles';
5 |
6 | const HoverUnderline = ({ className, children }) => {
7 | return cloneElement( Children.only(children), {
8 | className: cx(className, children.props.className, 'HoverUnderline')
9 | });
10 | };
11 |
12 | HoverUnderline.displayName = 'HoverUnderline';
13 |
14 | export default HoverUnderline;
15 |
--------------------------------------------------------------------------------
/app/modules/__Drawer__/Sort/SortPanel.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $drawer-sort-height: 150px
4 |
5 | .SortPanel
6 | width: 100%
7 | display: block
8 | height: $drawer-sort-height
9 |
10 | &:hover h3
11 | color: var(--primary)
12 | transition: color $key-animation-duration $key-bezier-animation-out
13 |
14 | h3
15 | margin-bottom: 6px
16 | font-size: $font-size-medium
17 | color: var(--textSecondary)
18 |
--------------------------------------------------------------------------------
/app/components/interaction/index.js:
--------------------------------------------------------------------------------
1 | export {default as ActionButton} from './ActionButton'
2 | export {default as Alert} from './Alert'
3 | export {default as HoverUnderline} from './HoverUnderline'
4 | export {default as Notification} from './Notification'
5 | export {default as TextSelectionPopup} from './TextSelectionPopup'
6 | export {default as Tooltip} from './Tooltip'
7 | export {default as Zoom} from './Zoom'
8 | export * from './Spinners'
9 | export * from './ContextMenus'
10 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderButtonIcon/HeaderButtonIcon.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .HeaderButtonIcon
4 | font-size: $subheader-icon-size
5 | margin: 0 4px
6 | padding: 2px 4px
7 | background: none
8 | outline: none
9 | color: var(--textSecondary)
10 | border: none
11 | line-height: 1
12 | vertical-align: center
13 | vertical-align: middle
14 | cursor: pointer
15 |
16 | &:hover
17 | color: var(--primary)
18 |
--------------------------------------------------------------------------------
/app/sass/base/_typography.sass:
--------------------------------------------------------------------------------
1 | /**
2 | * Typography
3 | *
4 | * Loops through a list of font names to generate application fonts
5 | */
6 |
7 | @mixin create-font($font-name)
8 | @font-face
9 | font-family: $font-name
10 | src: url( $font-dir + $font-name + '.ttf' ) format('truetype')
11 | font-weight: normal
12 | font-style: normal
13 |
14 | // see /utils/typography for `$font-list`
15 | @each $font in $font-list
16 | +create-font($font)
17 |
--------------------------------------------------------------------------------
/config/gulp/utils.js:
--------------------------------------------------------------------------------
1 | import { packageBuild } from './tasks'
2 |
3 | // Allows defining tasks in any order
4 | export const register = (gulp) => {
5 | gulp.registry(require("undertaker-forward-reference")());
6 | }
7 |
8 |
9 | /**
10 | * Small helper for injecting cmd arguments to packager
11 | * @return {Function}
12 | */
13 | export const createPackager = (platforms) => function packager(done) {
14 | return packageBuild(done, platforms.split(' ').map(p => `--${p}`));
15 | }
--------------------------------------------------------------------------------
/app/sass/README.md:
--------------------------------------------------------------------------------
1 | # sass
2 |
3 | This is the foundation of all Lurka's styles. There are two main sections:
4 |
5 | #### core
6 | Contains core styles that transpile to CSS. Imported once in `app/index.jsx`
7 |
8 | #### utils
9 | Contains sass abstractions that do not transpile to CSS.
10 |
11 | These utils are then imported throughout Lurka to create custom styles using shared code.
12 |
13 | ## variables.sass
14 | Contains CSS4 colors, layout dimensions and other shared variables.
15 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/containers/BoardSearch/BoardSearch.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $bg-color: var(--primary)
4 | $text-color: invert($bg-color)
5 |
6 | .BoardSearch
7 | background-color: $bg-color
8 | color: $text-color
9 | width: 68.1%
10 | height: 60px
11 | top: 60px
12 | left: 0
13 | right: 0
14 | margin: 0 auto
15 | position: absolute
16 | box-shadow: 0 1px 5px $grey-darkest
17 | border-radius: 6px
18 | text-align: center
19 |
--------------------------------------------------------------------------------
/app/modules/Panels/MenuPanel/components/MenuPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Scrollable, Title } from '~/components'
3 |
4 | const MenuPage = ({ title, children, className }) => {
5 | return (
6 |
7 |
8 | {title}
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
15 | export default MenuPage
16 |
--------------------------------------------------------------------------------
/app/components/__old__/Hierarchy/Hierarchy.jsx:
--------------------------------------------------------------------------------
1 | import './Hierarchy.styles'
2 | import React from "react";
3 |
4 | export default ({provider, boardID, threadID}) => {
5 | return (
6 |
7 | {provider && {provider} }
8 | {boardID && / {boardID} }
9 | {threadID && / {threadID} }
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/app/components/interaction/HoverUnderline/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .HoverUnderline
4 | position: relative
5 |
6 | &::before
7 | content: ""
8 | position: absolute
9 | bottom: -2px
10 | left: 0
11 | right: 0
12 | height: 2px
13 | width: 100%
14 | background: $text-primary
15 | transition: transform .24s
16 | transform: scaleX(0)
17 |
18 | &:hover::before
19 | transform: scaleX(1)
20 |
--------------------------------------------------------------------------------
/app/services/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Alerts from './Alerts'
4 | import ContextMenu from './ContextMenu'
5 | import Preloader from './Preloader'
6 | import Theme from './Theme'
7 | import Watcher from './Watcher'
8 |
9 | const {alertPosition} = window.Lurka;
10 |
11 | export default () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/app/components/media/ExpandedImage/ExpandedImage.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ExpandedImage
4 | // use flex to fix 7px extra height bug
5 | display: flex
6 | position: relative
7 |
8 | .ExpandedImage__blurred-thumbnail
9 | overflow: hidden
10 | filter: blur(10px)
11 | background-size: cover
12 | +size(100%)
13 | position: absolute
14 | top: 0
15 |
16 | .ExpandedImage__expanded
17 | position: relative
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/components/elements/Card/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Card
4 | display: inline-block
5 | padding: 1rem
6 | +box-shadow(1)
7 | // background: $grey-lightest
8 | transition: box-shadow .24s ease-out
9 | text-align: center
10 |
11 | &:hover
12 | +box-shadow(2)
13 |
14 |
15 | &.Card__size-medium
16 | padding: 1em
17 | margin: .5em
18 | +font-size(1)
19 |
20 | &.Card__is-active
21 | border: 2px solid $primary-dark
22 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Lurka
6 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/modules/Board/assemblies/PageGrid/PageGrid.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | // import {Page} from '../../components';
5 |
6 | const PageGrid = ({ className, children }) => {
7 | return (
8 | React.Children.map(children, (child, index) => {
9 |
10 | })
11 | );
12 | };
13 |
14 | PageGrid.displayName = 'PageGrid';
15 |
16 | PageGrid.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | export default PageGrid;
21 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderItem/HeaderItem.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .HeaderItem
4 | display: inline-block
5 |
6 | &.breadcrumb
7 | margin: 0
8 |
9 | &.shift-right
10 | margin-left: auto
11 |
12 | &.header-icon:last-child
13 | margin-right: 20px !important
14 |
15 | &:hover
16 | background: $grey-light
17 | border-color: var(--primary)
18 |
19 | &.active
20 | background: var(--primary)
21 |
--------------------------------------------------------------------------------
/app/components/other/Theme/injectTheme.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const injectTheme = (Component) => {
5 | return class ComponentWithTheme extends React.Component {
6 |
7 | static contextTypes = {
8 | theme: PropTypes.object.isRequired
9 | }
10 |
11 | render() {
12 | return
16 | }
17 | }
18 | }
19 |
20 | export default injectTheme;
21 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Overlay/Overlay.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ThreadOverlay
4 | +set-dims(100%, 100%)
5 | z-index: 10
6 | position: absolute
7 | background-color: $grey-darkest
8 | top: 0
9 | left: 0
10 | opacity: 0
11 | transition: opacity .2s ease-out
12 | pointer-events: none
13 | background-image: radial-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.9));
14 |
15 | &.Overlay-active
16 | pointer-events: auto
17 | opacity: .95
18 |
--------------------------------------------------------------------------------
/app/components/interaction/ActionButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 | import './styles';
4 | import { Button } from '~/components';
5 |
6 | const ActionButton = ({ className, children, ...restProps }) => {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | ActionButton.displayName = 'ActionButton';
15 |
16 | export default ActionButton;
17 |
--------------------------------------------------------------------------------
/app/modules/Cinema/components/CloseCinema.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import * as components from '~/components'
4 |
5 | const i = Lurka.icons;
6 |
7 | const CloseCinema = ({ className, onClick }) => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | CloseCinema.displayName = 'CloseCinema';
16 |
17 | export default CloseCinema;
18 |
--------------------------------------------------------------------------------
/app/sass/utils/functions.sass:
--------------------------------------------------------------------------------
1 | // Add percentage of white to a color
2 | @function tint($color, $percent)
3 | @return mix(white, $color, $percent)
4 |
5 | // Add percentage of black to a color
6 | @function shade($color, $percent)
7 | @return mix(black, $color, $percent)
8 |
9 | @function primary-opacity($opacity)
10 | @return rgba(red(color(primary)), green(color(primary)), blue(color(primary)), $opacity)
11 |
12 | @function calculateRem($size)
13 | $remSize: $size / 16px
14 | @return $remSize * 1rem
15 |
--------------------------------------------------------------------------------
/app/views/MediaView.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | import MediaViewer from '~/modules/MediaViewer';
5 |
6 | const MediaView = ({ className }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | MediaView.displayName = 'MediaView';
15 |
16 | MediaView.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | export default MediaView;
21 |
--------------------------------------------------------------------------------
/app/components/elements/Icon/Icon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from 'classnames'
3 |
4 | const packName = Lurka.iconPackName;
5 |
6 | export default function ( props ) {
7 | // add default class prefix eg 'mdi mdi-icon'
8 | const iconName = props.name ? `${packName} ${packName}-${props.name}` : ``
9 | const newProps = Object.assign({}, props, {
10 | className: classes('Icon', props.className, iconName)
11 | })
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/app/redux/selectors/watcher.js:
--------------------------------------------------------------------------------
1 | import {createSelector} from 'reselect';
2 |
3 | import {getThreadID} from './status';
4 |
5 | export const getWatchMetadata = (state) => state.watcher.entities.metadata;
6 | export const getWatchQueue = (state) => state.watcher.entities.queue;
7 | export const getWatchResults = (state) => state.watcher.entities.results;
8 |
9 | export const isCurrentThreadBeingWatched = createSelector(
10 | getThreadID,
11 | getWatchMetadata,
12 | (threadId, metadata) => !!metadata[threadId]
13 | )
14 |
--------------------------------------------------------------------------------
/app/sass/utils/z-index.sass:
--------------------------------------------------------------------------------
1 | // -------------
2 | // Layering (z-index)
3 | // -------------
4 |
5 | // Views
6 | $homepage-z-index: -1
7 | $contentpage-z-index: 10
8 | $headerpanelview-z-index: 20
9 | $preload-screen-z-index: 99999
10 |
11 | // Interface
12 | $mainheader-z-index: 600
13 | $subheader-z-index: 500
14 | $drawer-z-index: 500
15 |
16 | // Components:
17 | $tooltip-z-index: 1000
18 | $thread-tooltip-z-index: 10000
19 | $react-alerts-z-index: 9999
20 | $board-to-top-button: 10
21 | $thread-image-modal-z-index: 9998
22 |
--------------------------------------------------------------------------------
/app/components/elements/Title/classes.js:
--------------------------------------------------------------------------------
1 | import { createPropToClassMapper } from '~/utils/react';
2 |
3 | export default createPropToClassMapper("Title", {
4 | font: {
5 | primary: "font-primary",
6 | secondary: "font-secondary"
7 | },
8 | weight: {
9 | light: "weight-light",
10 | normal: "weight-normal",
11 | bold: "weight-bold"
12 | },
13 | align: {
14 | center: "align-center",
15 | left: "align-left",
16 | right: "align-right"
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 |
4 | language: node_js
5 |
6 | node_js:
7 | - 6
8 |
9 | before_install:
10 | npm run uninstall && npm cache clean --force
11 |
12 | install:
13 | npm install
14 |
15 | branches:
16 | only:
17 | - master
18 | - develop
19 | - travis-ci
20 |
21 | os:
22 | - linux
23 | - osx
24 |
25 | script:
26 | - npm run publish:travis
27 |
28 | addons:
29 | apt:
30 | packages:
31 | - build-essential
32 | - libxext-dev
33 | - libxtst-dev
34 | - libxkbfile-dev
--------------------------------------------------------------------------------
/app/components/interaction/Spinners/CircleSpinner/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import './Spinner.styles'
2 | import React from 'react';
3 | import classes from 'classnames';
4 |
5 | export default ({ isSpinning=true, className }) => {
6 | const spinnerClasses = classes("Spinner2", className, {
7 | "Spinner-active": isSpinning
8 | })
9 |
10 | return (
11 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/app/components/layout/Parallax/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ParallaxArea
4 | perspective: 1px
5 | height: 100%
6 | perspective-origin: right
7 | overflow-x: hidden
8 | width: 100%
9 |
10 | .ParallaxForeground, .ParallaxBackground
11 | position: absolute
12 | top: 0
13 | right: 0
14 | bottom: 0
15 | left: 0
16 | transform-origin: right
17 |
18 | .ParallaxBackground
19 | transform: translateZ(-1px) scale(2);
20 |
21 | .ParallaxForeground
22 | transform: translateZ(0);
23 |
--------------------------------------------------------------------------------
/app/modules/Board/tests/integration.tests.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Board from '..';
4 |
5 | describe("Integration", () => {
6 | global.$ = require('jquery');
7 | require('nanoscroller');
8 | require('velocity-animate');
9 | });
10 |
11 | it('renders without crashing', () => {
12 | const div = document.createElement('div');
13 | ReactDOM.render(
, div);
14 | });
15 |
16 | it('shallow render test', () => {
17 | shallow(
);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/app/modules/Cinema/components/NextMediaButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import * as components from '~/components'
4 |
5 | const i = Lurka.icons
6 |
7 | const NextMediaButton = ({ className, ...restProps }) => {
8 | return (
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | NextMediaButton.displayName = 'NextMediaButton';
16 |
17 | export default NextMediaButton;
18 |
--------------------------------------------------------------------------------
/config/api.4chan.js:
--------------------------------------------------------------------------------
1 | export default {
2 | boardlist: () =>
3 | `http://a.4cdn.org/boards.json`,
4 | board: (boardID) =>
5 | `http://a.4cdn.org/${boardID}/catalog.json`,
6 | thread: (boardID, threadID) =>
7 | `http://a.4cdn.org/${boardID}/thread/${threadID}.json`,
8 | archive: (boardID) =>
9 | `http://a.4cdn.org/${boardID}/archive.json`,
10 | media: (boardID) =>
11 | `https://i.4cdn.org/${boardID}/`,
12 | thumbnail: (boardID) =>
13 | `https://t.4cdn.org/${boardID}/`,
14 | }
15 |
--------------------------------------------------------------------------------
/config/proxy.js:
--------------------------------------------------------------------------------
1 | export default {
2 | headers: {
3 | "Host": "a.4cdn.org",
4 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0",
5 | "Accept": 'application/json, text/plain, */*',
6 | "Accept-Language": "en-GB,en;q=0.5",
7 | "Accept-Encoding": "gzip, deflate",
8 | "DNT": "1",
9 | "Connection": "keep-alive",
10 | "Upgrade-Insecure-Requests": "1",
11 | "Pragma": "no-cache",
12 | "Cache-Control": "no-cache"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/components/__old__/App/index.oldtest.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from '.';
4 |
5 | describe("App", () => {
6 | before(() => {
7 | global.$ = require('jquery');
8 | require('nanoscroller');
9 | require('velocity-animate');
10 | });
11 |
12 | it('renders without crashing', () => {
13 | const div = document.createElement('div');
14 | ReactDOM.render(
, div);
15 | });
16 |
17 | it('shallow render test', () => {
18 | shallow(
);
19 | });
20 | })
21 |
--------------------------------------------------------------------------------
/app/modules/index.js:
--------------------------------------------------------------------------------
1 | // export {default as Alerts} from './Alerts';
2 | // export {default as App} from './App';
3 | export {default as Board} from './Board';
4 | export {default as Cinema} from './Cinema';
5 | // export {default as Dashboard} from './Dashboard';
6 | export {default as Header} from './Header';
7 | export {default as Panels} from './Panels';
8 | export {default as Post} from './Post';
9 | export {default as Settings} from './Settings';
10 | export {default as Thread} from './Thread';
11 | // export {default as Watcher} from './Watcher';
12 |
--------------------------------------------------------------------------------
/app/sass/__themes__/dark/_greys.scss:
--------------------------------------------------------------------------------
1 | // Greys used for text and backgrounds alike. reversing the order will invert
2 | // the scheme
3 | $greys: (
4 | hsl(0, 0%, 96%), // 1
5 | hsl(0, 0%, 88%),
6 | hsl(0, 0%, 60%),
7 | hsl(0, 0%, 37%),
8 | hsl(0, 0%, 25%), // 5
9 | hsl(0, 0%, 19%),
10 | hsl(0, 0%, 16%),
11 | hsl(0, 0%, 13%),
12 | hsl(0, 0%, 10%),
13 | hsl(0, 0%, 7%), // 10
14 | );
15 |
16 | @function grey($index) {
17 | $grey: nth($greys, $index);
18 | @return warm-tint($grey); // or cool-tint()
19 | };
20 |
--------------------------------------------------------------------------------
/app/views/PostView.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import cx from 'classnames'
3 |
4 | import Post from '~/modules/Post';
5 |
6 | class PostView extends Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render() {
12 | const { className, } = this.props;
13 | return (
14 |
17 | );
18 | }
19 | }
20 |
21 | export default PostView;
22 |
--------------------------------------------------------------------------------
/app/components/elements/Overlay/Overlay.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Overlay
4 | +set-dims(100%, 100%)
5 | position: absolute
6 | background-color: $grey-darkest
7 | top: 0
8 | left: 0
9 | opacity: 0
10 | transition: opacity .2s ease-out
11 | pointer-events: none
12 | background-image: radial-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.9));
13 |
14 | &.Overlay-active
15 | pointer-events: auto
16 | opacity: .95
17 |
18 | &.fade
19 | pointer-events: auto
20 | opacity: .95
21 |
--------------------------------------------------------------------------------
/app/components/form/Dropdown/Dropdown.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Dropdown
4 | width: 100%
5 | user-select: none
6 |
7 | span.item
8 | color: var(--textPrimary)
9 | width: 100%
10 |
11 | > *
12 | width: 100%
13 | background-color: $dropdown-color
14 | border-bottom: $dropdown-color-hover solid 2px
15 | padding: 8px 10px
16 | cursor: pointer
17 |
18 | &:hover
19 | > *
20 | background-color: $dropdown-color-hover
21 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "react-hot-loader/babel",
4 | "transform-decorators-legacy",
5 | "transform-async-to-generator",
6 | ["module-resolver", {
7 | "alias": {
8 | "-": ".",
9 | "~": "./app",
10 | "package": "./package.json",
11 | "config": "./config",
12 | "sass": "./app/sass",
13 | "images": "./public/images",
14 | "shells": "./shells"
15 | }
16 | }]
17 | ],
18 | "presets": [
19 | "es2015",
20 | "react",
21 | "stage-1",
22 | "flow"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/app/components/form/SearchBarWithIcons/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .SearchBarWithIcons
4 | display: flex
5 | position: relative
6 | background-color: inherit
7 | +size(inherit, 3em)
8 | font-size: inherit
9 | line-height: 1
10 |
11 | .Icon
12 | padding: 1em
13 | background-color: inherit
14 |
15 | &.close-icon
16 | +clickable
17 | position: absolute
18 | right: 0
19 | height: inherit
20 |
21 | .Searchbar
22 | flex: 1
23 | padding-left: 0
24 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardSpinner/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $spinner-size: 60px
4 |
5 | .BoardSpinner
6 | width: 100vw
7 | display: flex
8 | justify-content: center
9 | margin-top: $spinner-size / 2
10 |
11 | .BoardSpinner__spinner
12 | +size($spinner-size)
13 | margin: 0 auto
14 |
15 | &::before
16 | content: "Loading..."
17 | font-size: 20px
18 | text-align: center
19 | position: relative
20 | bottom: -$spinner-size - 5px
21 | width: inherit
22 | left: -10px
23 |
--------------------------------------------------------------------------------
/app/modules/Board/components/NoSearchResults/NoSearchResults.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 | import cx from 'classnames';
3 |
4 | import './NoSearchResults.styles';
5 |
6 | const NoSearchResults = ({ className, children }) => {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | NoSearchResults.displayName = 'NoSearchResults';
15 |
16 | NoSearchResults.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | export default NoSearchResults;
21 |
--------------------------------------------------------------------------------
/app/modules/Cinema/components/PreviousMediaButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import * as components from '~/components'
4 |
5 | const i = Lurka.icons
6 |
7 | const PreviousMediaButton = ({ className, ...restProps }) => {
8 | return (
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | PreviousMediaButton.displayName = 'PreviousMediaButton';
16 |
17 | export default PreviousMediaButton;
18 |
--------------------------------------------------------------------------------
/app/sass/__themes__/dark/_base_colors.scss:
--------------------------------------------------------------------------------
1 | $base-colors: (
2 | punch: #EA4335, // $primary
3 | seagull: #6EC4E7, // $blue-light
4 | picton: #4EADF3, // $blue
5 | denim: #0E43A9, // triad of punch, use for tint?
6 | mantis: #8FCC6D, // $green
7 | swamp: #a3d08c, // $green
8 | fern: #55b359, // 4chan
9 | vermilion: #FF4500, // reddit
10 | contessa: #BF616A, // Nord
11 | horizon: #5e81ac, // Nord
12 | bouquet: #b48ead, // Nord
13 | putty: #EBCB8B, // Nord
14 | );
15 |
16 | // #E74C3C
17 |
--------------------------------------------------------------------------------
/app/services/ServiceComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | /**
4 | * ServiceComponent
5 | *
6 | * Classes that inherit from this do not render anything and do not update.
7 | * Instead they communicate on a "global" namespace using events.
8 | *
9 | * The events will trigger this service, like display an alert or toggle the
10 | * theme or analytics etc.
11 | */
12 | class ServiceComponent extends Component {
13 | render() { return null }
14 | shouldComponentUpdate() { return false }
15 | }
16 |
17 | export default ServiceComponent;
18 |
--------------------------------------------------------------------------------
/app/themes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Injects intitial theme into DOM.
3 | *
4 | * Tries to get theme from localstorage, otherwise loads
5 | * default built-in theme.
6 | */
7 | import CSSInjector from './CSSInjector';
8 | import registry from './registry';
9 | import utils from '~/utils';
10 |
11 | const customTheme = () => utils.localStorage.load('theme');
12 | const defaultTheme = () => registry.defaultTheme;
13 |
14 | const theme = customTheme() || defaultTheme();
15 |
16 | // Loads theme by injecting it into the DOM as CSS4 variables
17 | CSSInjector.injectTheme(theme);
18 |
--------------------------------------------------------------------------------
/app/components/media/Video/elements/FullScreen.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Icon} from '~/components'
3 |
4 | const i = Lurka.icons;
5 |
6 | export default ({ onClick, className, ariaLabel }) => {
7 | return (
8 |
9 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/app/themes/registry.js:
--------------------------------------------------------------------------------
1 | import * as palettes from "./palettes";
2 |
3 | /**
4 | * A registry containing all of the built-in themes for Lurka.
5 | */
6 | class Registry {
7 | static themes = {
8 | 'dark-red': palettes.darkRed
9 | };
10 |
11 | static getTheme(name) {
12 | if (name in this.themes) {
13 | return this.themes[name];
14 | }
15 |
16 | throw new Error(`No theme exists that is called '${name}'`);
17 | }
18 | }
19 |
20 | Registry.defaultTheme = Registry.getTheme(Lurka.defaultTheme);
21 |
22 | export default Registry
23 |
--------------------------------------------------------------------------------
/app/modules/Header/components/TitledIcon/TitledIcon.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .TitledIcon
4 | +clickable
5 | font-size: $subheader-font-size
6 | padding-right: 16px
7 | color: var(--textSecondary)
8 | transition: color .2s ease-out
9 |
10 | &:hover, &:active
11 | color: var(--textPrimary)
12 |
13 | .Icon
14 | color: var(--textPrimary)
15 |
16 | &:last-child
17 | padding-right: 0
18 | margin-right: 0
19 |
20 | .Icon
21 | font-size: $subheader-icon-size
22 | vertical-align: middle
23 |
--------------------------------------------------------------------------------
/app/modules/__Drawer__/Sort/SortIcon/SortIcon.jsx:
--------------------------------------------------------------------------------
1 | import './SortIcon.styles';
2 | import React, { PropTypes } from 'react';
3 | import cx from 'classnames';
4 |
5 | import {Icon} from '~/components'
6 |
7 | const SortIcon = ({ active, iconName, ...restProps }) => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | SortIcon.displayName = 'SortIcon';
16 |
17 | SortIcon.propTypes = {
18 | className: PropTypes.string,
19 | };
20 |
21 | export default SortIcon;
22 |
--------------------------------------------------------------------------------
/app/modules/Post/PostPositioner.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import cx from 'classnames';
3 |
4 | const PostPositioner = ({ className, children, centered, onClick }) => {
5 | const classes = cx('PostPositioner', className, {
6 | "position-left": !centered,
7 | "position-centered": centered
8 | });
9 |
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
17 | PostPositioner.defaultProps = {
18 | centered: true
19 | };
20 |
21 | export default PostPositioner
22 |
--------------------------------------------------------------------------------
/app/modules/Settings/components/Setting/Setting.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import classes from 'classnames'
3 |
4 |
5 | export default class Setting extends PureComponent {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render(){
11 | const { type, title, value, desc } = this.props.setting;
12 |
13 | return (
14 |
15 |
{title}
16 |
{String(value)}
17 |
{desc}
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/elements/ButtonWithPopout/index.jsx:
--------------------------------------------------------------------------------
1 | import './styles';
2 | import React from 'react';
3 | import cx from 'classnames';
4 | import { Button } from '~/components';
5 |
6 | const ButtonWithPopout = ({ className, popout, children, ...restProps }) => {
7 | return (
8 |
9 | {popout}
10 | {children}
11 |
12 | );
13 | };
14 |
15 | ButtonWithPopout.displayName = 'ButtonWithPopout';
16 |
17 | export default ButtonWithPopout;
18 |
--------------------------------------------------------------------------------
/app/components/layout/Container/Container.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | import './Container.styles';
5 |
6 | const Container = ({ className, children, padding }) => {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | Container.displayName = 'Container';
15 |
16 | Container.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | Container.defaultProps = {
21 | padding: "60px 15px 0"
22 | }
23 |
24 | export default Container;
25 |
--------------------------------------------------------------------------------
/app/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import store from '~/redux/store';
4 |
5 | // Views are the different "Pages" of the application
6 | import Views from '~/views';
7 |
8 | // Services are components that dont render anything.
9 | // E.g. CSS4 theme handler, preloader etc.
10 | import Services from '~/services'
11 |
12 | const App = ({store: testStore}) => (
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/index.js:
--------------------------------------------------------------------------------
1 | export {default as Overlay} from './Overlay'
2 | export {default as Comment} from './Comment'
3 | export {default as ThreadHeader} from './ThreadHeader'
4 | export {default as PostToolbar} from './PostToolbar'
5 | export {default as ThreadMedia} from './ThreadMedia'
6 | export {default as ThreadImage} from './ThreadImage'
7 |
8 | export {default as MediaInfo} from './MediaInfo'
9 | export {default as References} from './References'
10 | export {default as PostID} from './PostID'
11 |
12 | export {default as ControllerButton} from './ControllerButton'
13 | export * from './Controllers'
14 |
--------------------------------------------------------------------------------
/app/views/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './styles'
3 |
4 | // import DashboardView from './DashboardView';
5 | import ContentView from './ContentView';
6 | import CinemaView from './CinemaView';
7 | import PanelsView from './PanelsView';
8 | import PostView from './PostView';
9 |
10 | const Views = ({ id }) => {
11 | return (
12 |
18 | );
19 | };
20 |
21 | Views.displayName = 'Views';
22 |
23 | export default Views;
24 |
--------------------------------------------------------------------------------
/app/modules/Header/components/index.js:
--------------------------------------------------------------------------------
1 | export {default as BoardSpecs} from './BoardSpecs';
2 | export {default as ContentButtonGroup} from './ContentButtonGroup';
3 | export {default as FullLogo} from './FullLogo';
4 | export {default as HeaderGroup} from './HeaderGroup';
5 | export {default as HeaderTitle} from './HeaderTitle';
6 | export {default as IconGroup} from './IconGroup';
7 | export {default as SlideDownBG} from './SlideDownBG';
8 | export {default as TitledIcon} from './TitledIcon';
9 | export {default as VerticallyTitledIcon} from './VerticallyTitledIcon';
10 | export {default as HeaderButtonIcon} from './HeaderButtonIcon';
11 |
--------------------------------------------------------------------------------
/app/services/Preloader/connect.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 | import {
4 | fetchBoard,
5 | fetchBoardList,
6 | } from '~/redux/actions';
7 |
8 | function mapStateToProps({boardList, board, settings}) {
9 | return {
10 | board,
11 | boardList,
12 | defaultBoard: settings.homeBoard
13 | };
14 | }
15 |
16 | function mapDispatchToProps(dispatch) {
17 | return bindActionCreators({
18 | fetchBoardList,
19 | fetchBoard,
20 | }, dispatch)
21 | }
22 |
23 | export default connect(mapStateToProps, mapDispatchToProps);
24 |
--------------------------------------------------------------------------------
/app/modules/Post/connect.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | submitThreadPost,
6 | submitBoardPost
7 | } from '~/redux/actions';
8 |
9 | import {
10 | getCurrentBoardInfo
11 | } from '~/redux/selectors'
12 |
13 | function mapStateToProps(state) {
14 | return {
15 | currentBoardInfo: getCurrentBoardInfo(state)
16 | }
17 | }
18 |
19 | function mapDispatchToProps(dispatch) {
20 | return bindActionCreators({
21 | submitThreadPost
22 | }, dispatch)
23 | }
24 |
25 | export default connect(mapStateToProps, mapDispatchToProps)
26 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardPostImage.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import {Image} from '~/components';
3 |
4 | const BoardPostImage = ({ src, height, onLoad}) => {
5 | return !isNaN(height) ? (
6 |
7 |
12 |
13 | ) : false
14 | };
15 |
16 |
17 | BoardPostImage.displayName = 'BoardPostImage';
18 |
19 | BoardPostImage.propTypes = {
20 | className: PropTypes.string,
21 | };
22 |
23 | export default BoardPostImage;
24 |
--------------------------------------------------------------------------------
/app/modules/Header/components/IconGroup/HeaderIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import cx from 'classnames'
3 | import {
4 | HeaderItem,
5 | ButtonCircle,
6 | Icon
7 | } from '~/components'
8 |
9 | const HeaderIcon = ({
10 | name,
11 | onClick,
12 | active,
13 | ...restProps
14 | }) => {
15 | return (
16 |
19 |
22 |
23 | )
24 | }
25 |
26 | export default HeaderIcon
27 |
--------------------------------------------------------------------------------
/app/utils/designPatterns.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Allows only one instance of a class to be created. Uses a closure to reuse
3 | * the pattern with any class as an input.
4 | *
5 | * Usage:
6 | * const MySingletonClass = Singleton(Myclass);
7 | *
8 | * MySingletonClass() === MySingletonClass() // true
9 | *
10 | * @param {Class} Class
11 | * @return {Function} Class instance getter
12 | */
13 | export const Singleton = (Class) => (() => {
14 | var instance;
15 | return (...args) => {
16 | if (!instance) {
17 | instance = new Class(...args);
18 | }
19 | return instance;
20 | }
21 | })();
22 |
--------------------------------------------------------------------------------
/app/services/Watcher/connect.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | addWatchEntity,
6 | removeWatchEntity,
7 | updateWatchEntity
8 | } from '~/redux/actions';
9 |
10 |
11 | function mapStateToProps(state) {
12 | return {
13 | queue: state.watcher.entities.queue
14 | }
15 | }
16 |
17 | function mapDispatchToProps(dispatch) {
18 | return bindActionCreators({
19 | addWatchEntity,
20 | removeWatchEntity,
21 | updateWatchEntity
22 | }, dispatch)
23 | }
24 |
25 | export default connect(mapStateToProps, mapDispatchToProps)
26 |
--------------------------------------------------------------------------------
/app/modules/Header/components/SlideDownBG/SlideDownBG.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | import './SlideDownBG.styles';
5 |
6 | const SlideDownBG = ({ className, children, ...restProps }) => {
7 | return (
8 |
9 |
10 | {children}
11 |
12 |
13 | );
14 | };
15 |
16 | SlideDownBG.displayName = 'SlideDownBG';
17 |
18 | SlideDownBG.propTypes = {
19 | className: PropTypes.string,
20 | };
21 |
22 | export default SlideDownBG;
23 |
--------------------------------------------------------------------------------
/app/redux/actions/board/destroyBoard.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import { getBoardPosts } from '~/redux/selectors/board';
3 |
4 | export default function destroyBoard() {
5 | console.log("Action destroyBoard()");
6 |
7 | return (dispatch, getState) => {
8 | const state = getState()
9 | if ( shouldDestroyBoard(state) ) {
10 | console.warn("Board Destroyed! yay")
11 | dispatch(boardDestroyed());
12 | }
13 | }
14 | }
15 |
16 | export const shouldDestroyBoard = (state) => getBoardPosts(state).length > 0
17 |
18 | export const boardDestroyed = () => ({ type: types.BOARD_DESTROYED })
19 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_size.sass:
--------------------------------------------------------------------------------
1 | =size($width, $height: $width)
2 | width: $width
3 | height: $height
4 |
5 | =square($size)
6 | height: $size
7 | width: $size
8 |
9 | =circle($size)
10 | height: $size
11 | width: $size
12 | border-radius: 50%
13 |
14 | =limit-width($max, $min)
15 | max-width: $max
16 | min-width: $min
17 |
18 | =limit-height($max, $min)
19 | max-height: $max
20 | min-height: $min
21 |
22 | =set-dims($w, $h)
23 | width: $w
24 | height: $h
25 |
26 | =set-mins($w, $h)
27 | min-width: $w
28 | min-height: $h
29 |
30 | =set-max($w, $h)
31 | max-width: $w
32 | max-height: $h
33 |
--------------------------------------------------------------------------------
/app/services/Theme/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { onThemeChange } from '~/events';
3 | import ServiceComponent from '../ServiceComponent'
4 | import CSSInjector from '~/themes/CSSInjector'
5 |
6 | /**
7 | * Theme change handler
8 | * All injection/extraction passes through this class.
9 | */
10 | export class ThemeService extends ServiceComponent {
11 | @onThemeChange
12 | onThemeChange(themeObj) {
13 | CSSInjector.injectTheme(themeObj);
14 | const themeVars = CSSInjector.extractVariables();
15 | CSSInjector.saveTheme(themeVars);
16 | }
17 | }
18 |
19 | // Singleton
20 | export default ThemeService;
21 |
--------------------------------------------------------------------------------
/app/components/elements/IconCircle/IconCircle.jsx:
--------------------------------------------------------------------------------
1 | import './IconCircle.styles';
2 |
3 | import React, { PropTypes } from 'react';
4 | import cx from 'classnames';
5 |
6 | import ButtonCircle from '../ButtonCircle';
7 | import Icon from '../Icon';
8 |
9 | const IconCircle = ({ className, name, ...restProps }) => {
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | IconCircle.displayName = 'IconCircle';
18 |
19 | IconCircle.propTypes = {
20 | className: PropTypes.string,
21 | };
22 |
23 | export default IconCircle;
24 |
--------------------------------------------------------------------------------
/app/components/elements/LogoText/LogoText.jsx:
--------------------------------------------------------------------------------
1 | import './LogoText.styles';
2 | import React, { PropTypes } from 'react';
3 | import cx from 'classnames';
4 |
5 | import { name, version } from '-/package.json';
6 |
7 | const LogoText = ({ className }) => {
8 | return
9 |
10 |
{name}
11 |
v{version}
12 |
13 |
14 | }
15 |
16 | LogoText.displayName = 'LogoText';
17 |
18 | LogoText.propTypes = {
19 | className: PropTypes.string,
20 | };
21 |
22 | export default LogoText
23 |
--------------------------------------------------------------------------------
/app/components/interaction/Notification/Notification.jsx:
--------------------------------------------------------------------------------
1 | import './Notification.styles'
2 | import React from 'react';
3 | import cx from 'classnames'
4 |
5 | const Notification = ({ className, children, number=0 }) => {
6 | const notifClass = cx('Notification__content', className, {
7 | 'Notification__content--active': number > 0
8 | })
9 | return (
10 |
11 |
12 | {number}
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | Notification.displayName = 'Notification';
20 |
21 | export default Notification;
22 |
--------------------------------------------------------------------------------
/app/components/elements/Title/style.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 |
4 | h1
5 | +font-size(6)
6 | h2
7 | +font-size(4)
8 | h3
9 | +font-size(3)
10 | h4
11 | +font-size(2)
12 | h5
13 | +font-size(1)
14 | h6
15 | +font-size(0)
16 |
17 |
18 | .Title
19 | font-family: $font-main-light
20 | letter-spacing: .5rem
21 |
22 | &.weight-light
23 | font-weight: 400
24 | &.weight-normal
25 | font-weight: 500
26 | &.weight-bold
27 | font-weight: 600
28 | &.align-center
29 | text-align: center
30 | &.align-left
31 | text-align: left
32 | &.align-right
33 | text-align: right
34 |
35 |
--------------------------------------------------------------------------------
/app/components/other/Theme/ThemeProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Children } from "react";
2 | import {connect} from 'react-redux';
3 | import PropTypes from 'prop-types';
4 |
5 | class ThemeProvider extends Component {
6 | static propTypes = {
7 | theme: PropTypes.object.isRequired
8 | }
9 |
10 | static childContextTypes = {
11 | theme: PropTypes.object.isRequired
12 | }
13 |
14 | getChildContext() {
15 | return { theme: this.props.theme }
16 | }
17 |
18 | render() {
19 | return Children.only(this.props.children);
20 | }
21 | }
22 |
23 | export default connect(state => ({
24 | theme: state.settings.theme
25 | }))(ThemeProvider);
26 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/Dashboard.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Dashboard
4 | +size(100vw, 100vh)
5 |
6 | .header
7 | height: $header-height
8 | // background-color: $grey-dark
9 | padding: 12px 80px
10 | display: flex
11 | align-items: center
12 |
13 | .icon-button
14 | +clickable
15 | padding: 16px
16 | font-size: 24px
17 |
18 | .boards
19 | +size(100%, 100%)
20 | background-color: $grey-light
21 |
22 | .scrollable
23 | display: flex !important
24 | align-items: center
25 | justify-content: center
26 |
27 |
--------------------------------------------------------------------------------
/app/sass/__themes__/_registery.scss:
--------------------------------------------------------------------------------
1 | @import 'dark';
2 | @import 'light';
3 |
4 | $theme-registery: (
5 | dark: $dark-palette,
6 | light: $light-palette
7 | );
8 |
9 |
10 | // This is the function used throughout lurka to color every component
11 | @function theme($theme-name, $component-name, $shade: null) {
12 | $theme: map-get($theme-registery, $theme-name);
13 | $color: map-get($theme, $component-name);
14 |
15 | @if ($shade) {
16 | $shade-color: nth(color-shade($shade), 1);
17 | $percent: nth(color-shade($shade), 2);
18 |
19 | @return mix($shade-color, $color, $percent);
20 | }
21 |
22 | @return $color;
23 | };
24 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/containers/BoardSearch/BoardSearch.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import cx from 'classnames';
3 | // import './BoardSearch.styles';
4 |
5 | import { SearchBar } from '~/components';
6 |
7 | class BoardSearch extends Component {
8 | static propTypes = {
9 | className: PropTypes.string,
10 | };
11 |
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | search: ''
16 | };
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | )
23 | }
24 | }
25 |
26 | export default BoardSearch;
27 |
--------------------------------------------------------------------------------
/app/components/media/Video/elements/Download.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Icon} from '~/components'
3 |
4 | const i = Lurka.icons;
5 |
6 | export default ({ onClick, className, ariaLabel, downloadName, downloadLink }) => {
7 | return (
8 |
{e.stopPropagation(); console.log("click");}}>
9 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderButtonIcon/HeaderButtonIcon.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | import './HeaderButtonIcon.styles';
5 |
6 | import { Icon } from '~/components';
7 |
8 | const HeaderButtonIcon = ({ className, onClick, ...restProps }) => {
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | HeaderButtonIcon.displayName = 'HeaderButtonIcon';
17 |
18 | HeaderButtonIcon.propTypes = {
19 | className: PropTypes.string,
20 | };
21 |
22 | export default HeaderButtonIcon;
23 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Controllers/BookmarkController.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IconCircle } from '~/components'
3 |
4 | const i = Lurka.icons
5 | export default function(props) {
6 | // const { } = props;
7 |
8 | const defaultProps = {
9 | name: i.threadPostControlsBookmark
10 | }
11 |
12 | /*Save to archive
13 |
14 | bin icon -> to remove from archive
15 |
16 | OR use this instead of 'download':
17 |
18 |
19 | OR this. looks the best imo:
20 |
21 | */
22 | return
23 | }
24 |
--------------------------------------------------------------------------------
/app/modules/__Drawer__/Sort/SortIcon/SortIcon.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 |
4 | $drawer-sort-icon-size: 24px
5 | $drawer-sort-icon-width: 34px
6 |
7 | .SortIcon
8 | background-color: inherit
9 | color: var(--textSecondary)
10 | border-color: $header-border-color
11 | outline: none
12 | padding: 3px 5px
13 | box-shadow: 0 0 2px $grey-darkest
14 | margin-right: 8px
15 |
16 | &.active
17 | background: var(--primary)
18 | color: var(--textPrimary)
19 | border-color: transparent
20 | box-shadow: 0 0 2px var(--primaryDarkest)
21 |
22 | .Icon
23 | font-size: $drawer-sort-icon-size
24 | +center-icon
25 |
--------------------------------------------------------------------------------
/app/modules/Panels/components/Panel/Panel.jsx:
--------------------------------------------------------------------------------
1 | import './Panel.styles'
2 | import React, {Component} from 'react'
3 | import cx from 'classnames'
4 |
5 |
6 | export default function ({isDrawerOpen, children, isActive, isHidden, className}) {
7 | const panelClasses = cx('header-panel', className, {
8 | // compare directly because there will be scenarios with no
9 | // animate-in animate-out
10 | 'animate-in': isActive && !isHidden,
11 | 'animate-out': !isActive && !isHidden,
12 | 'shift-left': isDrawerOpen
13 | })
14 |
15 | return (
16 |
17 | {children}
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/modules/Board/components/index.js:
--------------------------------------------------------------------------------
1 | export {default as NoSearchResults} from './NoSearchResults'
2 | export {default as BoardPostHeader} from './BoardPostHeader'
3 | export {default as BoardSpinner} from './BoardSpinner'
4 | export {default as BoardPostComment} from './BoardPostComment'
5 | export {default as BoardPostImage} from './BoardPostImage'
6 | export {default as BoardSearch} from './BoardSearch'
7 | export {default as SortByArea} from './SortByArea'
8 | export {default as TitledIcon} from './TitledIcon'
9 | export {default as BoardToolbarMetadata} from './BoardToolbarMetadata'
10 | export {default as BoardMetadata} from './BoardMetadata'
11 | export {default as ToTopButton} from './ToTopButton'
12 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Controllers/ArchiveController.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IconCircle } from '~/components'
3 |
4 | export default function(props) {
5 | // const { } = props;
6 |
7 | const toggledProps = {
8 | name: "box"
9 | }
10 |
11 | const defaultProps = {
12 | name: "box"
13 | }
14 |
15 | /*Save to archive
16 |
17 | bin icon -> to remove from archive
18 |
19 | OR use this instead of 'download':
20 |
21 |
22 | OR this. looks the best imo:
23 |
24 | */
25 | return
26 | }
27 |
--------------------------------------------------------------------------------
/app/redux/actions/board/addBoardToFavourites.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import {
3 | isFavouriteBoard
4 | } from '~/redux/selectors';
5 |
6 | export default function addBoardToFavourites(boardID) {
7 | return (dispatch, getState) => {
8 | if (isFavouriteBoard(getState(), boardID)) {
9 | console.warn(`addBoardToFavourites(${boardID}) rejected: board is already in favourites`);
10 | return
11 | }
12 |
13 | dispatch(addFavouriteBoard(boardID));
14 | }
15 | }
16 |
17 | export const addFavouriteBoard = (boardID) => {
18 | return {
19 | type: types.BOARD_LIST_ADD_FAVOURITE,
20 | payload: boardID
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/sass/__themes__/_utils.scss:
--------------------------------------------------------------------------------
1 | @function warm-tint($color, $amount: 3) {
2 | @return adjust-color($color, $red: $amount, $blue: -$amount);
3 | }
4 |
5 | @function cold-tint($color, $amount: 3) {
6 | @return adjust-color($color, $red: -$amount, $blue: $amount);
7 | }
8 |
9 |
10 | // These values are used to create different cikir shades.
11 | // To see example of usage, check ./_registery.scss
12 |
13 | // values represent: (shade, amount to shade by)
14 | $color-shades: (
15 | darkest: (#000000, 20%),
16 | dark: (#5c5c5c, 20%),
17 | light: (#ffffff, 20%),
18 | lightest: (#ffffff, 40%),
19 | );
20 |
21 | @function color-shade($key) {
22 | @return map-get($color-shades, $key);
23 | };
24 |
--------------------------------------------------------------------------------
/app/modules/Header/components/HeaderGroup/HeaderGroup.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .HeaderGroup
4 |
5 | // &.MainHeader--left
6 | // margin-left: 0
7 | // margin-right: auto
8 | // > :last-child
9 | // margin-right: auto
10 |
11 | // &.MainHeader--right
12 | // padding-right: 0
13 | // margin-left: auto
14 | // > :first-child
15 | // margin-left: auto
16 |
17 | // &.MainHeader--center
18 | // margin: 0 auto
19 | // // align-self: center
20 | // text-align: center
21 | // flex: 1
22 |
23 | // > *
24 | // display: inline-block
25 |
--------------------------------------------------------------------------------
/app/redux/actions/board/removeBoardFromFavourites.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import {
3 | isFavouriteBoard
4 | } from '~/redux/selectors';
5 |
6 | export default function removeBoardFromFavourites(boardID) {
7 | return (dispatch, getState) => {
8 | if (!isFavouriteBoard(getState(), boardID)) {
9 | console.warn(`removeBoardFromFavourites(${boardID}) rejected: was already not in favovurites`);
10 | return
11 | }
12 |
13 | dispatch(removeFavouriteBoard(boardID));
14 | }
15 | }
16 |
17 | export const removeFavouriteBoard = (boardID) => {
18 | return {
19 | type: types.BOARD_LIST_REMOVE_FAVOURITE,
20 | payload: boardID
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/redux/reducers/postReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 | import initialState from '../initialState';
3 | import { createReducer, mergeState } from '~/utils/redux';
4 | import merge from 'updeep';
5 |
6 | export default createReducer(initialState.cinema, {
7 | [types.POST_OPENED]: (state, action) => {
8 | return merge({
9 | isOpen: true,
10 | position: action.payload.position,
11 | context: action.payload.context
12 | }, state);
13 | },
14 |
15 | [types.POST_CLOSED]: (state, action) => {
16 | return merge({
17 | isOpen: false,
18 | position: null,
19 | context: null
20 | }, state);
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/app/modules/Panels/components/SlideTransition/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 |
4 | .SlideTransition
5 | transition: transform $speed-fast
6 | transition-timing-function: $bezier-fast
7 |
8 | &.SlideTransition--animate-in
9 | transform: translate(0, 0)
10 |
11 | &.SlideTransition--animate-out
12 | transition-timing-function: $bezier-slow
13 |
14 | &.SlideTransition--from-left
15 | transform: translate(-100%, 0)
16 | &.SlideTransition--from-right
17 | transform: translate(100%, 0)
18 | &.SlideTransition--from-top
19 | transform: translate(0, -100%)
20 | &.SlideTransition--from-bottom
21 | transform: translate(0, 100%)
22 |
--------------------------------------------------------------------------------
/app/modules/Cinema/components/CurrentMedia.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import * as components from '~/components'
4 |
5 | const CurrentMedia = ({ srcLarge, thumbnail, className, filetype, onClick }) => {
6 | console.log("Hello")
7 | return (
8 |
9 | {filetype === ".webm"
10 | ?
11 | :
12 | }
13 |
14 | )
15 | }
16 |
17 | CurrentMedia.displayName = 'CurrentMedia';
18 |
19 | export default CurrentMedia;
20 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/References.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Icon, Line} from '~/components'
3 |
4 | const i = Lurka.icons;
5 |
6 | const References = ({ refs }) => {
7 | return refs && refs.length ?
8 |
9 |
10 | Replies ({refs.length})
11 |
12 | {refs.map( ref =>
13 |
14 |
15 | {`>>${ref}`}
16 |
17 |
18 | )}
19 |
: null
20 | }
21 |
22 | References.displayName = 'References';
23 |
24 | export default References;
25 |
--------------------------------------------------------------------------------
/config/webpack/vendors.js:
--------------------------------------------------------------------------------
1 | module.exports = module.exports.default = [
2 | "apicache",
3 | "axios",
4 | "classnames",
5 | "electron-squirrel-startup",
6 | "express",
7 | "fs-promise",
8 | "jquery",
9 | "moment",
10 | "mousetrap",
11 | "nanoscroller",
12 | "normalizr",
13 | "pub-sub-es6",
14 | "query-string",
15 | "react",
16 | "react-addons-css-transition-group",
17 | "react-alert",
18 | "react-dnd",
19 | "react-dnd-html5-backend",
20 | "react-dom",
21 | "react-html5video",
22 | "react-redux",
23 | "redux",
24 | "redux-logger",
25 | "redux-thunk",
26 | "request",
27 | "reselect",
28 | "screenfull",
29 | "uuid",
30 | "velocity-animate"
31 | ]
32 |
--------------------------------------------------------------------------------
/app/components/media/Video/elements/Time.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // http://stackoverflow.com/a/37770048
4 | const formatTime = (seconds) => {
5 | if (isNaN(seconds))
6 | return "0:00"
7 |
8 | let s = Math.floor(seconds)
9 | return (s-(s%=60)) / 60 + (
10 | 9 < s ? ':': ':0'
11 | ) + s
12 | };
13 |
14 | export default ({ currentTime, duration, className }) => {
15 | return (
16 |
17 |
18 | { formatTime(currentTime) }
19 |
20 | /
21 |
22 | { formatTime(duration) }
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/app/themes/utils.js:
--------------------------------------------------------------------------------
1 | import Color from 'color';
2 |
3 | export const warmTint = (color, scale=2) => {
4 | var color = Color(color);
5 |
6 | color.red(color.red() + 2 * scale);
7 | color.green(color.green() + 1 * scale);
8 | color.blue(color.blue() + -1 * scale);
9 |
10 | return toString(color)
11 | }
12 |
13 | export const coldTint = (color, scale=2) => {
14 | var color = Color(color);
15 |
16 | color.red(color.red() + -1 * scale);
17 | color.green(color.green() + 1 * scale);
18 | color.blue(color.blue() + 2 * scale);
19 |
20 | return toString(color)
21 | }
22 |
23 | function toString(color) {
24 | if (color.rgb().a) {
25 | return color.rgbaString()
26 | }
27 | return color.hexString()
28 | }
29 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/containers/BoardSelection/index.js:
--------------------------------------------------------------------------------
1 | import BoardSelection from './BoardSelection';
2 |
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | import {
7 | fetchBoard,
8 | addBoardToFavourites,
9 | destroyBoard
10 | } from '~/redux/actions';
11 |
12 | function mapStateToProps({ boardList, status }) {
13 | return {
14 | boardList,
15 | currentBoard: status.boardID
16 | }
17 | }
18 |
19 | function mapDispatchToProps(dispatch) {
20 | return bindActionCreators({
21 | fetchBoard,
22 | addBoardToFavourites,
23 | destroyBoard
24 | }, dispatch)
25 | }
26 |
27 | export default connect(mapStateToProps, mapDispatchToProps)(BoardSelection)
28 |
--------------------------------------------------------------------------------
/app/modules/Thread/events/media.js:
--------------------------------------------------------------------------------
1 | import screenfull from 'screenfull';
2 |
3 | // TODO: (media fullscreen) needs refactoring; use materialIcons and change img src toggle -> 2 images
4 | export default function setupFullscreen($thread) {
5 |
6 | $thread.on('click', '.fa-stack', function(event){
7 | event.stopPropagation()
8 | if (screenfull.enabled) {
9 | const target = $(event.target)
10 | .parents('.fullscreen')
11 | .next()
12 | console.warn(target);
13 | screenfull.toggle(target[0]);
14 | target.one('click', function(){
15 | screenfull.toggle(target[0]);
16 | });
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardPostHeader/BoardPostHeader.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | /* Counters + Icons */
4 | .BoardPostHeader
5 | user-select: none
6 | display: flex
7 |
8 | padding: 0 12px
9 | color: $text-secondary
10 | font-family: font-for(BoardPostHeader)
11 | +font-size(-2)
12 | font-variant: small-caps
13 |
14 | .counter
15 | display: inline-block
16 | padding: 0 3px
17 | line-height: 1
18 |
19 | &.timeago
20 | margin-right: auto
21 |
22 | &::first-letter
23 | text-transform: uppercase
24 |
25 | .Icon
26 | margin-right: 2px
27 | line-height: inherit
28 | &::before
29 | vertical-align: top
30 |
31 |
--------------------------------------------------------------------------------
/app/modules/Panels/MenuPanel/containers/BoardListPage/BoardListItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | const BoardListItem = (props) => {
5 | const { boardID, title, info, isFavourite } = props.board;
6 |
7 | return (
8 |
9 |
10 |
{title}
11 |
{boardID}
12 |
13 |
14 | {}
15 |
16 |
17 | );
18 | };
19 |
20 | BoardListItem.displayName = 'BoardListItem';
21 |
22 | export default BoardListItem;
23 |
--------------------------------------------------------------------------------
/app/components/media/ExpandedImage/ExpandedImage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './ExpandedImage.styles';
4 |
5 |
6 | const ExpandedImage = ({ className, srcThumbnail, srcExpanded, setReference, ...restProps }) => {
7 | const thumbnailStyles = {
8 | backgroundImage: `url(${srcThumbnail})`
9 | }
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | ExpandedImage.displayName = 'ExpandedImage';
20 |
21 | export default ExpandedImage;
22 |
--------------------------------------------------------------------------------
/app/modules/Board/components/TitledIcon/TitledIcon.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .TitledIcon
4 | +clickable
5 | font-size: $subheader-font-size
6 | color: var(--textSecondary)
7 | padding-right: 16px / $golden-ratio !important
8 | transition: color .2s ease-out
9 | display: inline-block
10 |
11 | &:hover, &:active
12 | transition: color .1s ease-out
13 | color: $primary-lightest !important
14 |
15 | .Icon
16 | color: $primary-light !important
17 |
18 | &:last-child
19 | padding-right: 0 !important
20 | margin-right: 0
21 |
22 | .Icon
23 | padding: 16px / ($golden-ratio * 2) !important
24 | font-size: $subheader-icon-size
25 | vertical-align: middle
26 |
--------------------------------------------------------------------------------
/app/redux/selectors/settings.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getSettings = state => state.settings;
4 |
5 | export const canSetSetting = (state, setting, value) => {
6 | const currentValue = getSettings()[setting];
7 |
8 | return value && currentValue !== value;
9 | }
10 |
11 | export const getHomeBoard = state => state.settings.homeBoard;
12 | export const getUserSettings = createSelector(getSettings, settings => {
13 | const userSettings = Object.assign({}, settings);
14 | delete userSettings['internal'];
15 | delete userSettings['settingDetails'];
16 | });
17 |
18 | export const getUserSettingsDetails = state => state.settings.details;
19 | export const getInternalSetting = (state, key) => state.settings.internal[key];
20 |
--------------------------------------------------------------------------------
/app/components/form/RadioGroup/RadioField.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | import Radio from './Radio'
5 |
6 | const RadioField = ({ className, label, isActive, onClick }) => {
7 | console.info(label);
8 | return (
9 |
12 |
13 |
14 | {label}
15 |
16 |
17 | );
18 | };
19 |
20 | RadioField.displayName = 'RadioField';
21 |
22 | RadioField.propTypes = {
23 | className: PropTypes.string,
24 | };
25 |
26 | export default RadioField;
27 |
--------------------------------------------------------------------------------
/app/components/media/Video/elements/PlayPause.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Icon} from '~/components'
3 |
4 | const i = Lurka.icons;
5 |
6 | export default ({ onClick, paused, className, ariaLabelPlay, ariaLabelPause }) => {
7 | return (
8 |
9 |
16 | { paused
17 | ?
18 | :
19 | }
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/app/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Usage:
3 | *
4 | * import utils from '/path/to/utils'
5 | *
6 | * utils.time.getShortTimeAgo
7 | */
8 |
9 | import * as conversions from './conversions'
10 | import * as dom from './dom'
11 | import * as localStorage from './localStorage'
12 | import * as react from './react'
13 | import * as redux from './redux'
14 | import * as throttle from './throttle'
15 | import * as time from './time'
16 | import * as types from './types'
17 | import * as designPatterns from './designPatterns'
18 |
19 |
20 | export default {
21 | conversions, dom, react, redux, throttle, time, types, localStorage, designPatterns
22 | }
23 |
24 |
25 | /* Don't import these - are imported elsewhere once only */
26 | // export * from './logger'
27 | // export * from './polyfills'
28 |
--------------------------------------------------------------------------------
/app/components/__old__/App/Routes.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import {
4 | Router,
5 | Route,
6 | IndexRoute,
7 | hashHistory
8 | } from "react-router";
9 |
10 | import HomeView from '~/views/HomeView';
11 | import ContentView from '~/views/ContentView';
12 | import SettingsView from '~/views/SettingsView';
13 |
14 | const Routes = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default Routes
27 |
--------------------------------------------------------------------------------
/app/components/layout/Parallax/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import './styles'
4 |
5 | export const Area = ({children, className}) => (
6 |
7 | {children}
8 |
9 | )
10 |
11 | Area.displayName = 'ParallaxArea';
12 |
13 |
14 | export const Foreground = ({children, className}) => (
15 |
16 | {children}
17 |
18 | )
19 |
20 | Foreground.displayName = 'ParallaxForeground';
21 |
22 |
23 | export const Background = ({children, className}) => (
24 |
25 | {children}
26 |
27 | )
28 |
29 | Background.displayName = 'ParallaxBackground';
30 |
--------------------------------------------------------------------------------
/app/redux/actions/settings/setSetting.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import {canSetSetting} from '~/redux/selectors/settings';
3 |
4 | export default function setSetting(key, value) {
5 | console.log("Action setSetting()", key, value);
6 |
7 | return (dispatch, getState) => {
8 | if (canSetSetting(getState(), key, value)) {
9 | console.warn(`Could not change setting '${key}' to '${value}'`);
10 | return
11 | }
12 |
13 | dispatch(settingChanged(key, value))
14 | console.info(`The setting ${key} was changed to ${value}`);
15 | }
16 | }
17 |
18 | export function settingChanged(setting, value) {
19 | return {
20 | type: types.SETTING_CHANGED,
21 | payload: { setting, value }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/components/elements/LogoText/LogoText.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .LogoText
4 | text-align: left
5 | position: relative
6 | top: -2px
7 | color: var(--primary)
8 | font-size: 22px
9 | height: 100%
10 |
11 | .center-content
12 | position: relative;
13 | top: 50%;
14 | transform: translateY(-50%);
15 |
16 | .name
17 | top: -2px
18 | position: relative
19 | font-size: 1em
20 | font-family: font-for(LogoText__name)
21 | letter-spacing: .075em
22 |
23 | .version
24 | letter-spacing: .075em
25 | font-size: 13px
26 | font-family: font-for(LogoText__version)
27 | color: var(--textSecondary)
28 | position: absolute
29 | top: 21px
30 | left: 4px
31 |
--------------------------------------------------------------------------------
/app/modules/Settings/index.js:
--------------------------------------------------------------------------------
1 | import Drawer from './Drawer';
2 |
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | import {
7 | setSetting
8 | } from '~/redux/actions';
9 |
10 | import {
11 | getUserSettings,
12 | getUserSettingsDetails,
13 | getInternalSetting
14 | } from '~/redux/selectors'
15 |
16 | function mapStateToProps(state) {
17 | return {
18 | settings: getUserSettings(state),
19 | settingDetails: getUserSettingsDetails(state),
20 | internalSettings: getInternalSetting(state),
21 | }
22 | }
23 |
24 | function mapDispatchToProps(dispatch) {
25 | return bindActionCreators({
26 | setSetting
27 | }, dispatch)
28 | }
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(Drawer)
31 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Controllers/CommentController.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon, ButtonCircle } from '~/components'
3 |
4 | export default function(props) {
5 | // const { } = props;
6 |
7 | const toggledProps = {
8 | name: "mesage-1"
9 | }
10 |
11 | const defaultProps = {
12 | name: "mesage-1"
13 | }
14 |
15 | {/* OR
16 | right facing:
17 |
18 |
19 |
20 | left facing:
21 |
22 |
23 |
addow facing left
24 |
25 | */}
26 |
27 | return
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/app/components/elements/index.js:
--------------------------------------------------------------------------------
1 | export {default as Button} from './Button'
2 | export {default as ButtonCircle} from './ButtonCircle'
3 | export {default as ButtonWithPopout} from './ButtonWithPopout'
4 | export {default as Card} from './Card'
5 | export {default as Counter} from './Counter'
6 | export {default as Elipses} from './Elipses'
7 | export {default as Icon} from './Icon'
8 | export {default as IconCircle} from './IconCircle'
9 | export {default as Logo} from './Logo'
10 | export {default as LogoText} from './LogoText'
11 | export {default as Overlay} from './Overlay'
12 | export {default as Pipe} from './Pipe'
13 | export * as SidePullout from './SidePullout'
14 | export {default as TimeAgo} from './TimeAgo'
15 | export {default as TimeAgoShort} from './TimeAgoShort'
16 | export {default as Title} from './Title'
17 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/Controllers/UpdateController.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon, ButtonCircle } from '~/components'
3 |
4 | export default function(props) {
5 | // const { } = props;
6 |
7 | const toggledProps = {
8 | name: "refresh-1"
9 | }
10 |
11 | const defaultProps = {
12 | name: "refresh-1"
13 | }
14 |
15 | {/* OR
16 | right facing:
17 |
18 |
19 |
20 | left facing:
21 |
22 |
23 |
addow facing left
24 |
25 | */}
26 |
27 | return
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/app/components/elements/ButtonWithPopout/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ButtonWithPopout
4 | position: relative
5 | line-height: 1em
6 |
7 | .popout
8 | opacity: 0
9 | transform: translateX(1em)
10 | position: absolute
11 | left: 1em
12 | top: 0
13 | bottom: 0
14 | margin: auto 0
15 | transition: transform .3s ease(back, out), opacity .3s ease(back, out)
16 | display: flex
17 | align-items: center
18 |
19 | .content
20 | transform: translateX(0)
21 | transition: transform .3s ease(back, out)
22 |
23 | &:hover, &:focus
24 | color: $text-primary
25 |
26 | .popout
27 | transform: translateX(0)
28 | opacity: 1
29 |
30 | .content
31 | transform: translateX(2/3*1em)
32 |
--------------------------------------------------------------------------------
/app/components/elements/Title/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './style'
3 | import mapPropsToClasses from './classes';
4 |
5 | const Title = ({ size=1, ...restProps }) => {
6 | restProps.className = mapPropsToClasses(restProps)
7 |
8 | switch (size) {
9 | case 1: return
10 | case 2: return
11 | case 3: return
12 | case 4: return
13 | case 5: return
14 | case 6: return
15 | default:
16 | throw new Error(`Title size ${size} doesn't exist`)
17 | }
18 | };
19 |
20 | Title.defaultProps = {
21 | font: "primary",
22 | weight: "normal",
23 | align: "center"
24 | }
25 |
26 | Title.displayName = 'Title';
27 |
28 | export default Title;
29 |
--------------------------------------------------------------------------------
/app/modules/Panels/MenuPanel/containers/BoardListPage/connect.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | getBoardListItems,
6 | getFavouriteBoards
7 | } from '~/redux/selectors';
8 |
9 | import {
10 | fetchBoard,
11 | addBoardToFavourites,
12 | removeBoardFromFavourites
13 | } from '~/redux/actions'
14 |
15 |
16 | function mapStateToProps(state) {
17 | return {
18 | boardList: getBoardListItems(state),
19 | favourites: getFavouriteBoards(state)
20 | }
21 | }
22 |
23 |
24 | function mapDispatchToProps(dispatch) {
25 | return bindActionCreators({
26 | fetchBoard,
27 | addBoardToFavourites,
28 | removeBoardFromFavourites
29 | }, dispatch)
30 | }
31 |
32 | export default connect(mapStateToProps, mapDispatchToProps)
33 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_position.sass:
--------------------------------------------------------------------------------
1 | %pos-horizon
2 | left: 0
3 | right: 0
4 |
5 | %pos-vertical
6 | top: 0
7 | bottom: 0
8 |
9 | =position($top:auto, $left:auto, $bottom:auto, $right:auto)
10 | top: $top
11 | left: $left
12 | bottom: $bottom
13 | right: $right
14 |
15 | =center($width, $height:$width)
16 | width: $width
17 | height: $height
18 | position: absolute
19 | margin: auto
20 | @extend %pos-horizon
21 | @extend %pos-vertical
22 |
23 | =center-horizontal($width)
24 | width: $width
25 | position: absolute
26 | margin: 0 auto
27 | @extend %cenH
28 |
29 | =center-vertical($height)
30 | height: $height
31 | position: relative
32 | transform: translateY(-50%)
33 |
34 | =absolute($top:auto, $left:auto)
35 | position: absolute
36 | top: $top
37 | left: $left
38 |
--------------------------------------------------------------------------------
/app/modules/Header/components/FullLogo/FullLogo.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 | import config from 'config';
4 |
5 | import './FullLogo.styles';
6 | import {
7 | Logo,
8 | LogoText
9 | } from '~/components';
10 |
11 | const FullLogo = ({ className }) => {
12 | return (
13 |
14 |
15 |
16 | Lurka
17 |
18 |
19 | v{config.meta.version}
20 |
21 |
22 | );
23 | };
24 |
25 | FullLogo.displayName = 'FullLogo';
26 |
27 | FullLogo.propTypes = {
28 | className: PropTypes.string,
29 | };
30 |
31 | export default FullLogo;
32 |
--------------------------------------------------------------------------------
/app/components/layout/LineBreak/Line.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $color: var(--divider)
4 |
5 | .Line
6 | display: block
7 | height: 1px
8 | border: 0
9 | background-color: $color
10 | margin: 0.8em 0 0.2em
11 | padding: 0
12 | position: relative
13 |
14 | &::before
15 | content: ""
16 | position: absolute
17 | top: 0
18 | height: 1px
19 | width: 60px
20 | left: -60px
21 | background-size: 100%
22 | background-image: linear-gradient(to left, $color, rgba(255,255,255,0))
23 |
24 | &::after
25 | content: ""
26 | position: absolute
27 | top: 0
28 | height: 1px
29 | width: 60px
30 | right: -60px
31 | background-size: 100%
32 | background-image: linear-gradient(to right, $color, rgba(255,255,255,0))
33 |
--------------------------------------------------------------------------------
/app/modules/Panels/__Panels.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 |
4 | import {
5 | WatchPanel,
6 | ArchivePanel
7 | } from './assemblies';
8 |
9 | const Panels = ({ className, isDrawerOpen, activePanel: panel, ...restProps}) => {
10 | console.log("Active panel is:", panel)
11 | const panelClass = cx('HeaderPanels', {'drawer-open': isDrawerOpen}, className)
12 | return (
13 |
17 | );
18 | };
19 |
20 | Panels.displayName = 'Panels';
21 |
22 | Panels.propTypes = {
23 | className: PropTypes.string,
24 | };
25 |
26 | export default Panels;
27 |
--------------------------------------------------------------------------------
/app/utils/redux.js:
--------------------------------------------------------------------------------
1 | import freeze from 'deep-freeze-node';
2 |
3 | /**
4 | * Uses a dictionary hash table rather than a switch statement to execute
5 | * reducers, which slightly increases performance on reducers with
6 | * multiple handlers
7 | */
8 | export const createReducer = (initialState, handlers) =>
9 | (state=initialState, action) =>
10 | action.type in handlers
11 | ? handlers[action.type](state, action)
12 | : state;
13 |
14 | /**
15 | * Extend a state object without mutating it.
16 | *
17 | * @params {Object} - Any number of objects that will be merged without
18 | * mutation. Last object takes merge priority
19 | * @return {Object}
20 | */
21 | export const mergeState = function() {
22 | const state = Object.assign.bind(null, {}).apply(null, arguments);
23 | return freeze(state);
24 | }
25 |
--------------------------------------------------------------------------------
/app/components/media/ImageWithChild/ImageWithChild.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 | import { Image } from '~/components';
4 |
5 | const ImageWithChild = ({ className, children, ...restProps }) => {
6 | return (
7 |
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | ImageWithChild.displayName = 'ImageWithChild';
15 |
16 | ImageWithChild.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | export default ImageWithChild;
21 |
22 |
23 | // TODO IDEA: Make thread watcher panel have a plus button that you can add the current thread with or add multiple boards (depending on location). MUST DO!!!
24 | // TODO IDEA: Instead of fullscreening image, make a mediaReel
25 |
--------------------------------------------------------------------------------
/app/views/__HomeView.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames'
3 | // import BoardSelection from '~/modules/BoardSelection';
4 | import Home from '~/modules/Home';
5 | import { Container } from '~/components';
6 | import './HomeView.styles';
7 |
8 | const HomeView = ({ className, children }) => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | {/* */}
17 |
18 | );
19 | };
20 |
21 | HomeView.displayName = 'HomeView';
22 |
23 | HomeView.propTypes = {
24 | className: PropTypes.string,
25 | };
26 |
27 | export default HomeView;
28 |
--------------------------------------------------------------------------------
/public/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Lurka
6 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardPostHeader/BoardPostHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | TimeAgo,
4 | Icon,
5 | Counter
6 | } from '~/components'
7 |
8 |
9 | import './BoardPostHeader.styles';
10 |
11 | const i = Lurka.icons
12 |
13 | export default ({time, replies}) => {
14 | return
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {/* */}
24 |
25 |
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/modules/Cinema/connect.jsx:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | toggleCinema,
6 | cycleCinemaTimeline
7 | } from '~/redux/actions';
8 |
9 | import {
10 | getBoardID,
11 | getBoardPosts,
12 | getBoardPostsBySearch,
13 | getBoardPostsByFilter,
14 | getBoardStatistics,
15 | isBoardBeingSearched,
16 | isBoardFetching,
17 | isBoardFiltered,
18 | } from '~/redux/selectors'
19 |
20 | function mapStateToProps(state) {
21 | return {
22 | isActive: state.cinema.isActive,
23 | timeline: state.cinema.entities.timeline
24 | }
25 | }
26 |
27 | function mapDispatchToProps(dispatch) {
28 | return bindActionCreators({
29 | toggleCinema,
30 | cycleCinemaTimeline
31 | }, dispatch)
32 | }
33 |
34 | export default connect(mapStateToProps, mapDispatchToProps)
35 |
--------------------------------------------------------------------------------
/app/components/__old__/ScrollableList/ScrollableList.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Children } from 'react';
2 | import cx from 'classnames';
3 |
4 | import './ScrollableList.styles';
5 | import { Scrollable } from '..';
6 |
7 | const ScrollableList = ({ className, children, ...restProps }) => {
8 | return (
9 |
10 |
11 | {Children.map(children, child => (
12 |
13 | {child}
14 |
15 | ))}
16 |
17 |
18 | );
19 | };
20 |
21 | ScrollableList.displayName = 'ScrollableList';
22 |
23 | ScrollableList.propTypes = {
24 | className: PropTypes.string,
25 | };
26 |
27 | export default ScrollableList;
28 |
--------------------------------------------------------------------------------
/app/sass/vendor/neutron/modules/_utilities.scss:
--------------------------------------------------------------------------------
1 | // Utilities
2 | //---------------------------------------------
3 |
4 | //hides the element when the
5 | //provided media query is supplied.
6 | @mixin hide($breakpoint: null) {
7 | @if $breakpoint {
8 | @include breakpoint($breakpoint) {
9 | & {
10 | display: none;
11 | }
12 | }
13 | } @else {
14 | display: none;
15 | }
16 | }
17 |
18 | //shows the element when the
19 | //provided media query is supplied.
20 | @mixin show($breakpoint: null) {
21 | @if $breakpoint {
22 | @include breakpoint($breakpoint) {
23 | & {
24 | display: block;
25 | display: initial;
26 | }
27 | }
28 | } @else {
29 | display: block;
30 | display: initial;
31 | }
32 |
33 | }
34 |
35 | //Adds block element self clearing
36 | @mixin clear-fix() {
37 |
38 | &:after {
39 | content: "";
40 | display: table;
41 | clear: both;
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/modules/Board/config.js:
--------------------------------------------------------------------------------
1 | const {
2 | boardPostMargin,
3 | headerHeight,
4 | expandedHeaderHeight,
5 | subheaderHeight,
6 | settingsWidth,
7 | boardOuterMargin
8 | } = Lurka.settings;
9 |
10 |
11 | export const masonryGrid = {
12 | targetSelector: '.BoardPost',
13 | containerSelector: '#board',
14 | margin: boardPostMargin,
15 | gutterLeft: boardOuterMargin,
16 | gutterRight: boardOuterMargin,
17 | gutterTop: 0
18 | }
19 |
20 | export const masonryGridWithDrawer = Object.assign({}, masonryGrid, {
21 | gutterRight: settingsWidth + boardOuterMargin
22 | })
23 |
24 | export const nano = {
25 | sliderMaxHeight: 400,
26 | sliderMinHeight: 50
27 | }
28 |
29 | export const scroll = {
30 | // headerToggleOffset: 264
31 | headerToggleOffset: 386
32 | }
33 |
34 | export default {
35 | masonryGrid,
36 | masonryGridWithDrawer,
37 | nano,
38 | scroll
39 | }
40 |
--------------------------------------------------------------------------------
/app/components/elements/TimeAgoShort/TimeAgo.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, PropTypes } from 'react';
2 | import cx from 'classnames';
3 | import { getShortTimeAgo } from '~/utils/time';
4 |
5 | class TimeAgo extends PureComponent {
6 | constructor(props) {
7 | super(props)
8 | this.update = this.update.bind(this)
9 | this._interval = setInterval(this.update, 1000)
10 |
11 | this.state = {
12 | clicked: false
13 | }
14 | }
15 |
16 | componentWillUnmount() {
17 | clearInterval(this._interval);
18 | }
19 |
20 | render() {
21 | const { className, time, ...restProps} = this.props;
22 |
23 | return
24 | {getShortTimeAgo(time)}
25 |
26 | }
27 |
28 | update() {
29 | this.forceUpdate();
30 | }
31 | }
32 |
33 | export default TimeAgo
34 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/components/HomeBoard/HomeBoard.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | import {Icon} from '~/components';
5 |
6 | import './HomeBoard.styles';
7 |
8 | const i = Lurka.icons;
9 |
10 | const HomeBoard = ({ className, onClick, homeBoard }) => {
11 | return (
12 |
13 |
14 | { !!homeBoard ? (
15 |
16 | {homeBoard}
17 |
18 | ) : (
19 |
20 | )}
21 |
22 |
23 | );
24 | };
25 |
26 | HomeBoard.displayName = 'HomeBoard';
27 |
28 | HomeBoard.propTypes = {
29 | className: PropTypes.string,
30 | };
31 |
32 | export default HomeBoard;
33 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/MediaInfo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon } from '~/components'
3 | import { setHTML } from '~/utils/react'
4 |
5 | const MediaInfo = ({ media }) => {
6 | if (!media)
7 | return null
8 |
9 | const { filename, filetype } = media
10 |
11 | let iconName;
12 | if (filetype.includes("webm"))
13 | iconName = 'filmstrip'
14 | else
15 | // iconName = 'image-area'
16 | iconName = 'file-image'
17 |
18 | let fName = filename
19 | if (fName.length > 25)
20 | fName = fName.slice(0, 25) + '(...)'
21 | fName += filetype
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | )
29 |
30 | }
31 |
32 | MediaInfo.displayName = 'MediaInfo';
33 |
34 | export default MediaInfo;
35 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardPostComment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { setHTML } from '~/utils/react';
3 | import { BoardPostHeader as PostHeader } from '.';
4 | import { Line } from '~/components';
5 |
6 | const BoardPostComment = ({ title, comment, time, replies }) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | { title &&
14 |
15 | }
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | BoardPostComment.displayName = 'BoardPostComment';
24 |
25 | export default BoardPostComment;
26 |
--------------------------------------------------------------------------------
/app/views/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $bg-1: $grey
4 | $bg-2: $grey-dark
5 |
6 |
7 | .Views
8 | position: relative
9 |
10 | .View
11 | // background: $grey-dark
12 | // background: linear-gradient(135deg, $bg-2, $bg-1, $bg-2)
13 | // background: url('~/images/backgrounds/dark_wall.png')
14 | // position: fixed
15 |
16 | .DashboardView
17 | +set-dims(100vw, 100vh)
18 | z-index: $homepage-z-index
19 | position: fixed
20 |
21 | .ContentView
22 | +set-dims(100vw, 100vh)
23 | position: fixed
24 | // z-index: $contentpage-z-index
25 |
26 | .PanelsView
27 | z-index: $headerpanelview-z-index
28 | z-index: 500
29 | position: relative
30 | top: $header-height
31 | // +size(100vw, 100vh)
32 | top: $header-height + $subheader-height
33 |
34 | // &.PanelsView--header-expanded
35 |
36 | .SettingsView
37 | +set-dims(100vw, 100vh)
38 | z-index: 100
39 | top: -100%
40 |
--------------------------------------------------------------------------------
/app/sass/__themes__/dark/_palette.scss:
--------------------------------------------------------------------------------
1 | // Main color palette
2 | $dark-palette: (
3 | primary: $primary,
4 | secondary: $secondary,
5 | links: c(picton),
6 | greentext: c(swamp),
7 | fourchan: c(fern),
8 | reddit: c(vermilion),
9 | favourite: c(putty),
10 | anon-name: c(putty),
11 | content-border: $grey,
12 | layout: $primary,
13 | text-primary: rgba(255,255,255, 1),
14 | text-secondary: rgba(255,255,255, 0.7),
15 | text-disabled: rgba(255,255,255, 0.5),
16 | divider: rgba(255,255,255, 0.12),
17 | highlight: mix($primary, $grey, 10%),
18 | boardpost: $grey-dark,
19 | threadpost: $grey,
20 | settings-drawer: $grey-dark,
21 | board: $grey,
22 | text-selection: $primary,
23 | thread-quote: #da7c7b,
24 | );
25 | // layout: # c3362b,
26 |
--------------------------------------------------------------------------------
/app/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import apiReducer from "./apiReducer";
4 | import boardReducer from "./boardReducer";
5 | import boardlistReducer from "./boardListReducer";
6 | import cinemaReducer from "./cinemaReducer";
7 | import cacheReducer from "./cacheReducer";
8 | import threadReducer from "./threadReducer";
9 | import watcherReducer from "./watcherReducer";
10 | import postReducer from "./postReducer";
11 | import settingsReducer from "./settingsReducer";
12 | import statusReducer from "./statusReducer";
13 |
14 | const rootReducer = combineReducers({
15 | api: apiReducer,
16 | boardList: boardlistReducer,
17 | board: boardReducer,
18 | cinema: cinemaReducer,
19 | thread: threadReducer,
20 | cache: cacheReducer,
21 | post: postReducer,
22 | status: statusReducer,
23 | settings: settingsReducer,
24 | watcher: watcherReducer,
25 | });
26 |
27 | export default rootReducer
28 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | // This binds the root directory to path.join, so it can be used to join paths
2 | // relative to the root dir
3 |
4 | var pathToRootRelative = '../';
5 | var join = require('path').join.bind(null, __dirname, pathToRootRelative);
6 |
7 | var paths = {
8 | root: join(),
9 | app: join('app'),
10 | app_entry: join('app', 'index.jsx'),
11 | build: join('build'),
12 | app_modules: join('build', 'node_modules'),
13 | app_bundle: join('build', 'app.bundle.js'),
14 | dist: join('dist'),
15 | electron_entry: join('electron', 'index.js'),
16 | logo: join('public', 'images', 'logo.png'),
17 | app_html: join('public', 'main.html'),
18 | app_preloader: join('public', 'preloader.html'),
19 | github_token: join('github_token.txt') // you have to make this to be able to publish
20 | }
21 |
22 | module.exports = module.exports.default = paths;
23 |
--------------------------------------------------------------------------------
/app/components/App.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | // var x = shallow(
);
6 | // var x = render(
).children();
7 | // console.error(Object.keys(x));
8 | // for (let u in x) {
9 | // console.error(u);
10 | // }
11 | // console.error(x);
12 | // throw new Error();
13 | describe("App", () => {
14 | // before(() => {
15 | // global.$ = require('jquery');
16 | // require('nanoscroller');
17 | // require('velocity-animate');
18 | // });
19 |
20 | it('renders without crashing', () => {
21 | const div = document.createElement('div');
22 | render(
, div);
23 | });
24 |
25 | it('mounts without crashing', () => {
26 | const div = document.createElement('div');
27 | var wrapper = mount(
, {attachTo: div});
28 | wrapper.detach()
29 | });
30 |
31 | it('shallow render test', () => {
32 | shallow(
);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/index.js:
--------------------------------------------------------------------------------
1 | import Dashboard from './Dashboard';
2 | export {Dashboard as default}; // comment this out when ready
3 |
4 | import { bindActionCreators } from 'redux';
5 | import { connect } from 'react-redux';
6 |
7 | import {
8 | getBoardID,
9 | getHomeBoard,
10 | getBoardsOrderedAlphabetically,
11 | getFavouriteBoards
12 | } from '~/redux/selectors';
13 |
14 | import {
15 | fetchBoard,
16 | addBoardToFavourites,
17 | removeBoardFromFavourites
18 | } from '~/redux/actions';
19 |
20 |
21 | function mapStateToProps(state) {
22 | return {
23 | currentBoard: getBoardID(state),
24 | homeBoard: getHomeBoard(state),
25 | }
26 | }
27 |
28 | function mapDispatchToProps(dispatch) {
29 | return bindActionCreators({
30 | fetchBoard,
31 | addBoardToFavourites
32 | }, dispatch);
33 | }
34 |
35 | // export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
36 |
37 |
--------------------------------------------------------------------------------
/app/redux/reducers/cacheReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 | import initialState from '../initialState';
3 | import { createReducer, mergeState } from '~/utils/redux';
4 |
5 | export default createReducer(initialState.cache, {
6 | [types.BOARD_CACHED]: (state, action) =>
7 | mergeState(state, {
8 | board: mergeState(state.board, {
9 | [action.boardID]: action.payload
10 | })
11 | }),
12 |
13 | [types.THREAD_CACHED]: (state, action) =>
14 | mergeState(state, {
15 | thread: mergeState(state.thread, {
16 | [action.threadID]: action.payload
17 | })
18 | }),
19 |
20 | [types.BOARD_CACHE_CLEARED]: (state, action) =>
21 | mergeState(state, {
22 | board: {}
23 | }),
24 |
25 | [types.THREAD_CACHE_CLEARED]: (state, action) =>
26 | mergeState(state, {
27 | thread: {}
28 | }),
29 | });
30 |
--------------------------------------------------------------------------------
/app/components/form/SearchBar/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Searchbar
4 | height: 100%
5 | width: 100%
6 | transition: opacity .3s ease
7 | display: inline-block
8 | border: none
9 | line-height: inherit
10 | background: inherit
11 | color: inherit
12 | border-radius: inherit
13 | text-align: inherit
14 | padding-left: 16px
15 | backface-visibility: hidden
16 |
17 | &:focus::-webkit-input-placeholder
18 | transition: opacity 0.5s ease
19 | opacity: 0
20 |
21 |
22 | /*
23 | .Icon
24 | line-height: inherit
25 | display: inline-block
26 | padding: 6px 16px
27 |
28 | &.search
29 | color: inherit
30 |
31 | &.close
32 | position: absolute
33 | right: 0
34 | color: #d55250
35 |
36 | font-family: font-for(Searchbar)
37 | line-height: 100%
38 | display: flex
39 | align-items: center
40 | */
41 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/containers/BoardSearch/index.js:
--------------------------------------------------------------------------------
1 | import BoardSearch from './BoardSearch';
2 |
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | import {
7 | getBoardID,
8 | getHomeBoard,
9 | getBoardsOrderedAlphabetically,
10 | getFavouriteBoards
11 | } from '~/redux/selectors';
12 |
13 | import {
14 | fetchBoard,
15 | addBoardToFavourites,
16 | removeBoardFromFavourites
17 | } from '~/redux/actions';
18 |
19 |
20 | function mapStateToProps(state) {
21 | return {
22 | currentBoard: getBoardID(state),
23 | orderedBoards: getBoardsOrderedAlphabetically(state)
24 | }
25 | }
26 |
27 | function mapDispatchToProps(dispatch) {
28 | return bindActionCreators({
29 | fetchBoard,
30 | addBoardToFavourites,
31 | removeBoardFromFavourites
32 | }, dispatch);
33 | }
34 |
35 | export default connect(mapStateToProps, mapDispatchToProps)(BoardSearch);
36 |
--------------------------------------------------------------------------------
/config/webpack/index.js:
--------------------------------------------------------------------------------
1 | require('babel-register');
2 |
3 | module.exports = module.exports.default = function getConfig(env) {
4 | // Note: All configs run in production, except for dev
5 |
6 | switch (env) {
7 | /**
8 | * App Configs
9 | */
10 | case "dev":
11 | case "development":
12 | return require('./webpack.dev');
13 |
14 | case "preview":
15 | return require('./webpack.preview');
16 |
17 | case "prod":
18 | case "production":
19 | return require('./webpack.prod');
20 |
21 | case "test":
22 | return require('./webpack.test');
23 |
24 | /**
25 | * Shell configs
26 | */
27 | case "electron":
28 | return require('./webpack.electron');
29 |
30 | case "chrome":
31 | return require('./webpack.chrome');
32 | }
33 |
34 | throw new Error(`No webpack config found for env '${env}'`);
35 | }
36 |
--------------------------------------------------------------------------------
/app/components/media/Video/elements/Overlay.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import {
3 | Icon,
4 | VideoSpinner
5 | } from '~/components'
6 |
7 | const i = Lurka.icons;
8 |
9 | const OverlayIcon = ({ error, paused, loading }) => {
10 | if (!error && !loading || paused)
11 | return null
12 |
13 | return
14 | { error ?
15 | : loading ?
16 | : paused ?
17 | : null
18 | }
19 |
20 | }
21 |
22 | const VideoOverlay = ({ onClick, error, paused, loading }) => {
23 | return (
24 |
26 |
31 |
32 | );
33 | }
34 |
35 | export default VideoOverlay
36 |
--------------------------------------------------------------------------------
/app/modules/Panels/WatchPanel/connect.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | fetchThread,
6 | addWatchEntity,
7 | removeWatchEntity,
8 | } from '~/redux/actions';
9 |
10 | import {
11 | getBoardID,
12 | getThreadID,
13 | getWatchQueue,
14 | getWatchResults,
15 | getWatchMetadata
16 | } from '~/redux/selectors';
17 |
18 | function mapStateToProps(state) {
19 | return {
20 | boardID: getBoardID(state),
21 | threadID: getThreadID(state),
22 | queue: getWatchQueue(state),
23 | results: getWatchResults(state),
24 | metadata: getWatchMetadata(state)
25 | }
26 | }
27 |
28 | function mapDispatchToProps(dispatch) {
29 | return bindActionCreators({
30 | fetchThread,
31 | addWatchEntity,
32 | removeWatchEntity
33 | }, dispatch)
34 | }
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})
37 |
--------------------------------------------------------------------------------
/app/services/README:
--------------------------------------------------------------------------------
1 | # Lurka/Services
2 |
3 | ### What they do
4 | Services are components that are attached to the React DOM tree but do not render any html.
5 |
6 | They listen to events and perform different functions based on the service type.
7 |
8 | # Examples
9 |
10 | ### Watcher
11 | Polls a url at given intervals and triggers an action when the url's content is updated via the "Last-Modified" http header.
12 | Is used to watch 4chan threads in the background to allow the user to be notified when an update occurs.
13 |
14 | ### Preloader
15 | Loads the initial content if none is available. Performs two requests: get the 4chan boardList, and get the "default" board to start on.
16 |
17 | ### Theme
18 | Injects CSS4 variables into the DOM and listens for an updateTheme event which will save the updated theme in the browser (localStorage)
19 |
20 | ### Context Menu
21 | Listens for a Open/Close event and appends a context menu to the DOM then positions it in relation to the users cursor.
22 |
23 |
--------------------------------------------------------------------------------
/app/modules/Board/components/BoardMetadata/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import './styles';
5 |
6 | const BoardMetadata = ({ className, prefix="Viewing", suffix="threads", postsShown=0, totalPosts=0, totalImages, totalReplies }) => {
7 | return (
8 |
9 |
10 | {prefix} {postsShown} of {totalPosts} {suffix}
11 |
12 | {totalImages && totalReplies &&
13 |
14 | Replies: {totalReplies}
15 | Images: {totalImages}
16 | {/*Shitpost ratio: {totalImages ? Math.round((totalReplies / totalImages) * 100) / 100 : 0} */}
17 |
}
18 |
19 | );
20 | };
21 |
22 | BoardMetadata.displayName = 'BoardMetadata';
23 |
24 | export default BoardMetadata;
25 |
--------------------------------------------------------------------------------
/config/mocha/helpers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import path from 'path';
4 | import paths from 'config/paths';
5 | import glob from 'glob';
6 | import ReactTestRenderer from 'react-test-renderer';
7 |
8 |
9 |
10 | export const shallow = (Component) => {
11 | const renderer = ReactTestRenderer.create(Component);
12 |
13 | renderer.render(component);
14 |
15 | return {
16 | output: renderer.getRenderOutput(),
17 | renderer
18 | };
19 | };
20 |
21 |
22 | export const createSuite = (name, testRunner) => {
23 | return describe.bind(null, name, testRunner);
24 | }
25 |
26 | export const jsdom = require('mocha-jsdom');
27 |
28 | export const discoverTests = (cwd, ...pattern) => {
29 | if (Object.prototype.toString.call(pattern) === '[object Array]') {
30 | pattern = `{${pattern.join(',')}}`;
31 | }
32 |
33 | var files = glob.sync(pattern, {cwd: cwd})
34 | throw new Error(files);
35 | }
36 |
37 | export const injectMockStore = (Component) => {
38 | return
39 | }
40 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/containers/BoardList/index.js:
--------------------------------------------------------------------------------
1 | import BoardList from './BoardList';
2 |
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | import {
7 | getBoardID,
8 | getHomeBoard,
9 | getBoardsOrderedAlphabetically,
10 | getFavouriteBoards,
11 | } from '~/redux/selectors';
12 |
13 | import {
14 | fetchBoard,
15 | addBoardToFavourites,
16 | removeBoardFromFavourites
17 | } from '~/redux/actions';
18 |
19 |
20 | function mapStateToProps(state) {
21 | return {
22 | currentBoard: getBoardID(state),
23 | orderedBoards: getBoardsOrderedAlphabetically(state),
24 | favouriteBoards: getFavouriteBoards(state)
25 | }
26 | }
27 |
28 | function mapDispatchToProps(dispatch) {
29 | return bindActionCreators({
30 | fetchBoard,
31 | addBoardToFavourites,
32 | removeBoardFromFavourites
33 | }, dispatch);
34 | }
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(BoardList);
37 |
--------------------------------------------------------------------------------
/app/utils/conversions.js:
--------------------------------------------------------------------------------
1 | const KB = 1024
2 | const MB = 1048576
3 | const GB = 1073741824
4 |
5 | /**
6 | * Converts bytes into human readable formats
7 | * @param {Integer} bytes
8 | * @return {Object}
9 | */
10 | export const convertBytes = (bytes) => {
11 | return {
12 | kilobytes: (bytes/KB).toFixed(2)+" KB",
13 | megabytes: (bytes/MB).toFixed(2)+" MB",
14 | gigabytes: (bytes/GB).toFixed(2)+" GB"
15 | }
16 | }
17 |
18 | /**
19 | * returns a string split by commas every 3 digits from the right.
20 | * e.g. 12345 returns "12,345"
21 | * @param {Integer}
22 | * @return {String}
23 | */
24 | export const commaify = (int) => {
25 | return String(int)
26 | .split('').reverse().join('') // reverse string
27 | .match(/[\s\S]{1,3}/g).join(',') // split into groups of 3 from left (from right)
28 | .split('').reverse().join('') // reverse string again
29 | }
30 |
31 |
32 | export const toDecimal = (num, dp=1) => Math.round(num * (dp * 10)) / (dp * 10)
33 |
--------------------------------------------------------------------------------
/.appveyor.yml:
--------------------------------------------------------------------------------
1 | os: unstable
2 |
3 | # Test against this version of Node.js
4 | environment:
5 | # Is Encrypted:
6 | GH_TOKEN: x2jkNPuuroRIwLHO8xpFuOLprcGnTbokQnFF3gWAQUstPdCUDVQIu+bs19OytDy8
7 | matrix:
8 | # node.js
9 | - nodejs_version: "6"
10 |
11 | # Install scripts. (runs after repo cloning)
12 | install:
13 | # Get the latest stable version of Node.js or io.js
14 | - ps: Install-Product node $env:nodejs_version
15 | - set CI=true
16 | - set DEBUG=electron-builder
17 | - set PATH=%APPDATA%\npm;%PATH%
18 | # install modules
19 | - yarn
20 |
21 | cache:
22 | - "%LOCALAPPDATA%\\Yarn"
23 |
24 | matrix:
25 | fast_finish: true
26 |
27 | branches:
28 | only:
29 | - master
30 | - develop
31 | - appveyor
32 |
33 | build: off
34 | version: '{build}'
35 | shallow_clone: true
36 | clone_depth: 1
37 |
38 | # Post-install test scripts.
39 | test_script:
40 | # Output useful info for debugging.
41 | - node --version
42 | - npm --version
43 | - npm test
44 | - npm run publish:appveyor
45 |
--------------------------------------------------------------------------------
/app/sass/utils/mixins/_shadow.sass:
--------------------------------------------------------------------------------
1 | /**
2 | * +box-shadow($depth: 1-5)
3 | *
4 | * Gives a box shadow effect using material ui depth guidelines.
5 | */
6 | @mixin box-shadow($depth, $inset: null)
7 | @if $depth < 1
8 | box-shadow: none
9 | @else if $depth > 5
10 | @error "Invalid $depth `#{$depth}` for mixin `box-shadow`. Expecting 1-5."
11 | @else if $inset != null
12 | box-shadow: top-shadow($depth) inset
13 | @else
14 | box-shadow: bottom-shadow($depth), top-shadow($depth)
15 |
16 | @function top-shadow($depth)
17 | $primary-offset: nth(1.5 3 10 14 19, $depth) * 1px
18 | $blur: nth(1.5 3 10 14 19, $depth) * 4px
19 | $color: rgba(black, nth(.12 .16 .19 .25 .30, $depth))
20 |
21 | @return 0 $primary-offset $blur $color
22 |
23 | @function bottom-shadow($depth)
24 | $primary-offset: nth(1.5 3 6 10 15, $depth) * 1px
25 | $blur: nth(1 3 3 5 6, $depth) * 4px
26 | $color: rgba(black, nth(.24 .23 .23 .22 .22, $depth))
27 |
28 | @return 0 $primary-offset $blur $color
29 |
--------------------------------------------------------------------------------
/app/redux/actions/thread/monitorThread.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import { alertMessage } from '../alert';
3 |
4 | export function monitorThread(thread, boardID) {
5 | console.log("Action monitorThread");
6 | const { threadID } = thread
7 |
8 | return (dispatch, getState) => {
9 | if (threadBeingMonitored(threadID, getState())) {
10 | dispatch(alertMessage({
11 | 'type': 'info',
12 | 'message': `Thread #${threadID} is already being watched`,
13 | 'duration': 8000
14 | }))
15 | return
16 | }
17 |
18 | dispatch(alertMessage({
19 | 'type': 'success',
20 | 'message': `Monitoring current thread (#${threadID})`,
21 | 'duration': 8000
22 | }))
23 |
24 | dispatch(watchThread(thread))
25 | }
26 | }
27 |
28 |
29 | export function watchThread(thread) {
30 | return {
31 | type: types.THREAD_MONITOR_ADDED,
32 | payload: thread
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/electron/events/handleBeforeSendHeaders.js:
--------------------------------------------------------------------------------
1 | const onBeforeSendHeaders = (details, callback) => {
2 | if (process.env.NODE_ENV === "development" && !details.url.includes('4cdn')) {
3 | // webpack-dev-server doesn't like its headers played with
4 | return callback({ cancel:false })
5 | }
6 |
7 | // Bypass 4chan media block (occurs when referer is localhost)
8 | if (details.url.includes('a.4cdn.org')) {
9 | details.requestHeaders['Host'] = "a.4cdn.org";
10 | details.requestHeaders['Origin'] = "http://boards.4chan.org";
11 | } else {
12 | details.requestHeaders['Host'] = "i.4cdn.org";
13 | }
14 |
15 | details.requestHeaders['User-Agent'] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0";
16 | details.requestHeaders['DNT'] = 1;
17 | details.requestHeaders['Referer'] = undefined;
18 |
19 | callback({
20 | cancel: false,
21 | requestHeaders: details.requestHeaders
22 | });
23 | }
24 |
25 | export default onBeforeSendHeaders;
26 |
--------------------------------------------------------------------------------
/app/redux/actions/watcher/removeWatchEntity.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types';
2 | import { alertMessage } from '../alert';
3 |
4 | const {alerts} = Lurka;
5 |
6 | export default function removeWatchEntity({ id }) {
7 | console.log("Action removeWatchEntity");
8 |
9 | return (dispatch, getState) => {
10 | const state = getState();
11 |
12 | if (!isRegistered(id, state)) {
13 | const message = alerts.watchEntityAlreadyRemoved(id);
14 |
15 | console.error(message);
16 | dispatch(alertMessage(message));
17 |
18 | return
19 | }
20 |
21 | dispatch(createRemoveWatchEntity({ id }))
22 | }
23 | }
24 |
25 | export function isRegistered(id, state) {
26 | // TODO: replace this with a selector
27 | return state.watcher.entities.queue.find( entity => entity.id === id );
28 | }
29 |
30 |
31 | export function createRemoveWatchEntity(payload) {
32 | return {
33 | type: types.WATCH_ENTITY_REMOVED,
34 | payload: payload
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/modules/Dashboard/components/BoardListItem/BoardListItem.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .BoardListItem
4 | +clickable
5 | display: flex
6 | padding: 4px 0
7 | background-color: $grey-dark
8 | transition: background-color .15s .05s $bezier-slow
9 |
10 |
11 | &:hover
12 | transition: background-color .2s $bezier-fast
13 | background-color: var(--primaryDarkest)
14 |
15 | .BoardListItem__title-wrap
16 | display: inline-block
17 | min-width: 230px
18 |
19 | .BoardListItem__title
20 | font-size: $font-size-large
21 | font-family: font-for(BoardListItem__title)
22 | padding: 12px 24px
23 | text-align: right
24 |
25 | .BoardListItem__metadata-wrap
26 | display: inline-block
27 | .BoardListItem__description
28 | .BoardListItem__tags
29 | display: flex
30 |
31 | .BoardListItem__tag
32 | font-family: font-for(BoardListItem__tag)
33 | font-size: $font-size-small
34 | color: var(--textSecondary)
35 | padding-right: 12px
36 |
--------------------------------------------------------------------------------
/app/modules/Panels/BookmarksPanel/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import cx from 'classnames';
3 | import './styles';
4 |
5 | import {ClassTransition} from '../components';
6 | import {emitCloseHeaderPanel} from '~/events';
7 |
8 | class BookmarksPanel extends Component {
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | setTransitionerRef = (ref) => this.transitioner = ref;
14 |
15 | // Used by parent to control UI
16 | show (args){ this.transitioner.show(args) }
17 | hide (args){ this.transitioner.hide(args) }
18 |
19 | render() {
20 | const { className, } = this.props;
21 | console.warn("BookmarksPanel.render()")
22 | return (
23 |
24 |
25 | BookmarksPanel!
26 |
27 |
28 | );
29 | }
30 |
31 |
32 | }
33 |
34 | export default BookmarksPanel;
35 |
--------------------------------------------------------------------------------
/app/modules/Thread/containers/ThreadControls/ThreadControls.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | $thread-controls-gutter-size: 12px
4 | $thread-controls-height: 65px
5 |
6 | .thread-controls
7 | position: fixed
8 | width: 100vw
9 | bottom: 0
10 | left: 0
11 | z-index: 20
12 | height: 1px
13 | opacity: 1
14 |
15 | &.animate-in
16 | transform: translateY(0)
17 | transition: transform .65s ease
18 | opacity: 1
19 |
20 | &.animate-out
21 | transform: translateY($thread-controls-height)
22 | transition: transform .2s $bezier-fast
23 | transition-property: transform, opacity
24 | opacity: 0
25 |
26 | .left-controls
27 | left: 0
28 |
29 | .ButtonCircle
30 | float: right
31 |
32 |
33 | .right-controls
34 | right: 0
35 |
36 | .controls
37 | width: calc((100vw - #{$thread-width}) / 2)
38 | position: fixed
39 | height: $thread-controls-height
40 | bottom: 0
41 | padding: 0 $thread-controls-gutter-size
42 |
--------------------------------------------------------------------------------
/app/modules/Thread/components/ThreadMedia/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .ThreadMedia
4 | display: flex
5 | margin-bottom: 4px
6 |
7 | &:not(.toggled)
8 | margin-right: 10px
9 | float: left
10 |
11 | &.toggled
12 | margin-bottom: 0.8em
13 | display: inline-block
14 |
15 |
16 | video, img
17 | +clickable
18 | max-width: 100%
19 | align-self: center
20 |
21 | .ThreadMedia--expanded
22 | display: inline-block
23 | // TODO: dynamic class setting on thread images; limit height
24 | max-height: calc(100vh - $header-height)
25 |
26 | .ThreadMedia__FullscreenIcon
27 | +clickable
28 | display: none
29 | position: absolute
30 | top: 0
31 | right: 0
32 | margin: 0
33 |
34 | .Icon
35 | text-shadow: 0 0 0 $grey
36 |
37 | &:hover
38 | text-shadow: 0 0 1px $grey-dark
39 |
40 | .ThreadMedia--IWC:hover
41 | .ThreadMedia__FullscreenIcon
42 | display: initial
43 |
--------------------------------------------------------------------------------
/app/components/elements/SidePullout/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .SidePulloutArea
4 | display: flex
5 | position: fixed
6 | +size(auto, 100vh)
7 | top: 0
8 | left: 0
9 | flex-direction: column
10 | justify-content: space-around
11 | z-index: 999999999
12 |
13 | .SidePulloutItem
14 | position: absolute
15 | display: flex
16 | flex-direction: row
17 | height: 3em
18 | line-height: 3em
19 | white-space: nowrap
20 | background: $grey-dark
21 | transition: transform .3s $easeInCubic
22 | +clickable
23 |
24 | &.left
25 | flex-direction: row-reverse
26 | transform: translateX(-100%)
27 |
28 | &.right
29 | right: 0
30 |
31 | &:hover
32 | transition: transform .3s .05s $easeInOutCubic
33 | transform: translateX(0)
34 |
35 | .SidePulloutHandle
36 | +padding(0, 2)
37 | background: $primary
38 | position: absolute
39 | right: 0
40 | transform: translateX(100%)
41 |
42 | .SidePulloutContent
43 | +padding(0, 3, 0, 4)
44 | display: inline-block
45 |
--------------------------------------------------------------------------------
/app/components/other/ToggleOnClick/ToggleOnClick.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import classes from 'classnames';
3 |
4 |
5 | export default class ToggleOnClick extends PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.toggle = this.toggle.bind(this)
9 |
10 | this.state = {
11 | isExpanded: false
12 | }
13 | }
14 |
15 | render() {
16 | const {isExpanded} = this.state;
17 | const {from: _from, to, className, ...rest} = this.props
18 | const toggleClasses = classes(className, {
19 | 'toggled': isExpanded,
20 | })
21 |
22 | return
23 | {isExpanded ? to : _from }
24 |
25 | }
26 |
27 | toggle() {
28 | this.setState(state => {
29 | console.log('ToggleOnClick() clicked. expanding ?', state.isExpanded)
30 | return {
31 | isExpanded: !state.isExpanded
32 | }
33 | })
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/app/components/interaction/Spinners/CircleSpinner/Spinner.styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Spinner2
4 | +center(60px, 60px)
5 | position: fixed
6 | z-index: 10
7 | display: none
8 |
9 | &.Spinner-active
10 | display: initial !important
11 |
12 | .double-bounce1, .double-bounce2
13 | +size(inherit)
14 | border-radius: 50%
15 | background-color: $primary
16 | position: absolute
17 | top: 0
18 | left: 0
19 |
20 | animation: sk-bounce 1s infinite ease-in-out
21 |
22 |
23 | .double-bounce2
24 | animation-delay: -.5s
25 | background-color: $primary-darkest
26 |
27 |
28 | @-webkit-keyframes sk-bounce
29 | 0%, 100%
30 | -webkit-transform: scale(0.0)
31 | 50%
32 | -webkit-transform: scale(1.0)
33 |
34 |
35 | @keyframes sk-bounce
36 | 0%, 100%
37 | transform: scale(0.0)
38 | -webkit-transform: scale(0.0)
39 | 50%
40 | transform: scale(1.0)
41 | -webkit-transform: scale(1.0)
42 |
43 |
--------------------------------------------------------------------------------
/app/components/form/Checkbox/styles.sass:
--------------------------------------------------------------------------------
1 | @import '~sass/utils'
2 |
3 | .Checkbox
4 | width: 20px
5 | position: relative
6 | display: inline
7 | margin-left: 6px
8 |
9 | label
10 | +square(20px)
11 | cursor: pointer
12 | position: absolute
13 | top: 0
14 | left: 0
15 | background: linear-gradient(to top, $grey-darkest, $grey-dark)
16 | border-radius: 4px
17 | box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0 0 4px rgba(255,255,255,.3)
18 | &::after
19 | content: ''
20 | width: 9px
21 | height: 5px
22 | position: absolute
23 | top: 4px
24 | left: 4px
25 | border: 3px solid #fcfff4
26 | border-top: none
27 | border-right: none
28 | background: transparent
29 | opacity: 0
30 | transform: rotate(-45deg)
31 |
32 | &:hover::after
33 | opacity: 0.3
34 |
35 | input[type=checkbox]
36 | visibility: hidden
37 | &:checked + label::after
38 | opacity: 1
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/redux/actions/cache/cacheCurrentThread.js:
--------------------------------------------------------------------------------
1 | import * as types from '~/redux/types'
2 | import {
3 | getThreadID,
4 | getThreadPosts,
5 | getThreadReceivedAt
6 | } from '~/redux/selectors'
7 |
8 | export default function cacheCurrentThread () {
9 | return function(dispatch, getState) {
10 | const state = getState();
11 |
12 | const threadID = getThreadID(state);
13 | const threadPosts = getThreadPosts(state);
14 | const threadReceivedAt = getThreadReceivedAt(state);
15 |
16 | if (threadPosts && threadPosts.length) {
17 | dispatch(threadCached(threadID, threadPosts, threadReceivedAt));
18 | }
19 |
20 | else {
21 | console.error(`Could not cache invalid thread. Was: ${threadPosts}`);
22 | }
23 | }
24 | }
25 |
26 |
27 | export function threadCached (threadID, threadPosts, threadReceivedAt) {
28 | return {
29 | type: types.THREAD_CACHED,
30 | threadID: threadID,
31 | payload: {
32 | posts: threadPosts,
33 | receivedAt: threadReceivedAt
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/utils/dom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Traverses up the DOM until an element is found that contains the passed in
3 | * class
4 | *
5 | * @param {Object} el - The initial DOM element reference
6 | * @param {String} clss - Name of the class to check for
7 | * @return {Object} - The first DOM element that has the specified class
8 | */
9 | export const findParentWithClass = (el, clss) => {
10 | while (true) {
11 | if (el.classList.contains(clss)) {
12 | break;
13 | }
14 | el = el.parentNode;
15 | }
16 | return el;
17 | }
18 |
19 |
20 | // http://stackoverflow.com/a/7557433
21 | export const isElementInViewport = (el) => {
22 | const rect = el.getBoundingClientRect();
23 |
24 | return (
25 | rect.top >= 0 &&
26 | rect.left >= 0 &&
27 | rect.bottom <= window.innerHeight &&
28 | rect.right <= window.innerWidth
29 | );
30 | }
31 |
32 | export const isElementPartiallyInViewport = (el) => {
33 | const rect = el.getBoundingClientRect();
34 |
35 | return (
36 | rect.top <= window.innerHeight &&
37 | rect.bottom >= 0
38 | );
39 | }
40 |
--------------------------------------------------------------------------------